smart-linux-assistant 0.8.0__tar.gz → 0.8.2__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.
- {smart_linux_assistant-0.8.0/smart_linux_assistant.egg-info → smart_linux_assistant-0.8.2}/PKG-INFO +36 -3
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/README.md +35 -2
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/cli/main.py +27 -3
- smart_linux_assistant-0.8.2/linux_assistant/config/defaults.py +37 -0
- smart_linux_assistant-0.8.2/linux_assistant/config/loader.py +64 -0
- smart_linux_assistant-0.8.2/linux_assistant/config/models.py +55 -0
- smart_linux_assistant-0.8.2/linux_assistant/config/validator.py +154 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/explainer.py +10 -7
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/search.py +5 -3
- smart_linux_assistant-0.8.2/linux_assistant/utils/__init__.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/utils/groq_client.py +6 -6
- smart_linux_assistant-0.8.2/linux_assistant/utils/redactor.py +37 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/pyproject.toml +1 -1
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2/smart_linux_assistant.egg-info}/PKG-INFO +36 -3
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/smart_linux_assistant.egg-info/SOURCES.txt +6 -0
- smart_linux_assistant-0.8.2/tests/test_config_validator.py +57 -0
- smart_linux_assistant-0.8.0/linux_assistant/utils/redactor.py +0 -25
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/LICENSE +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/__init__.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/cli/__init__.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/config/__init__.py +0 -0
- /smart_linux_assistant-0.8.0/linux_assistant/core/__init__.py → /smart_linux_assistant-0.8.2/linux_assistant/config/exceptions.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/config/settings.py +0 -0
- {smart_linux_assistant-0.8.0/linux_assistant/utils → smart_linux_assistant-0.8.2/linux_assistant/core}/__init__.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/core/safety.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/exceptions/__init__.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/exceptions/base.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/models/__init__.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/models/command_result.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/models/history_entry.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/repositories/__init__.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/repositories/history_repository.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/__init__.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/command_executor.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/utils/logger.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/utils/shell.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/setup.cfg +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/smart_linux_assistant.egg-info/dependency_links.txt +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/smart_linux_assistant.egg-info/entry_points.txt +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/smart_linux_assistant.egg-info/requires.txt +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/smart_linux_assistant.egg-info/top_level.txt +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_cli.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_command_executor.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_explainer.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_groq_client.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_history_repository.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_redactor.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_safety.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_search.py +0 -0
- {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_shell.py +0 -0
{smart_linux_assistant-0.8.0/smart_linux_assistant.egg-info → smart_linux_assistant-0.8.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smart-linux-assistant
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: An AI-powered Linux productivity assistant for command analysis, troubleshooting, and knowledge management.
|
|
5
5
|
Author: Shubham Kumar Jha
|
|
6
6
|
License: MIT
|
|
@@ -100,6 +100,37 @@ This project does not require any environment variables for its core CLI functio
|
|
|
100
100
|
| --- | --- | --- |
|
|
101
101
|
| (none) | No required environment variables for CLI execution | - |
|
|
102
102
|
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
Smart Linux Assistant supports a user configuration file. If no configuration file is present, the application automatically falls back to built-in defaults.
|
|
107
|
+
|
|
108
|
+
By default, the application looks for:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
~/.config/smart-linux-assistant/config.toml
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A complete example configuration is available in the repository as:
|
|
115
|
+
```
|
|
116
|
+
config.toml.example
|
|
117
|
+
```
|
|
118
|
+
Copy it into your local configuration directory and modify only the values you want to customize.
|
|
119
|
+
Example:
|
|
120
|
+
```
|
|
121
|
+
mkdir -p ~/.config/smart-linux-assistant
|
|
122
|
+
cp config.toml.example ~/.config/smart-linux-assistant/config.toml
|
|
123
|
+
```
|
|
124
|
+
Current configurable options include:
|
|
125
|
+
|
|
126
|
+
- AI provider
|
|
127
|
+
- AI model
|
|
128
|
+
- API timeout
|
|
129
|
+
- Retry count
|
|
130
|
+
- Command history settings
|
|
131
|
+
- Logging verbosity
|
|
132
|
+
- Privacy (secret redaction)
|
|
133
|
+
|
|
103
134
|
## Usage / API Reference
|
|
104
135
|
|
|
105
136
|
The project exposes the console scripts `smart-linux` and `sla` (configured in `pyproject.toml`).
|
|
@@ -220,9 +251,11 @@ Suggested fix:
|
|
|
220
251
|
|
|
221
252
|
## Privacy Note
|
|
222
253
|
|
|
223
|
-
The `explain`, `fix`, and `search` commands send
|
|
254
|
+
The `explain`, `fix`, and `search` commands send your terminal queries to Groq's API for processing.
|
|
255
|
+
|
|
256
|
+
**Important update starting in v0.7.0+:** To provide context-aware solutions, the `fix` command securely reads your 5 most recent local commands and injects them into the Groq API prompt.
|
|
224
257
|
|
|
225
|
-
Security First
|
|
258
|
+
**Security First:** Before any history leaves your machine, it passes through a local Regex Redactor (`linux_assistant.utils.redactor`) which automatically scrubs standard environment variables, inline passwords, AWS keys, and Bearer tokens, replacing them with `[REDACTED]`. However, you should still exercise caution and avoid running AI commands immediately after working with highly sensitive, non-standard plaintext secrets.
|
|
226
259
|
|
|
227
260
|
Separately, `smart-linux run` records your command invocations (command text, exit code, duration, working directory, and truncated stderr) in a local SQLite database. `stdout` is never recorded. To disable history recording entirely, set `SMART_LINUX_NO_HISTORY=1`. To view or erase recorded history, use `smart-linux history` and `smart-linux history clear`.
|
|
228
261
|
|
|
@@ -73,6 +73,37 @@ This project does not require any environment variables for its core CLI functio
|
|
|
73
73
|
| --- | --- | --- |
|
|
74
74
|
| (none) | No required environment variables for CLI execution | - |
|
|
75
75
|
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
Smart Linux Assistant supports a user configuration file. If no configuration file is present, the application automatically falls back to built-in defaults.
|
|
80
|
+
|
|
81
|
+
By default, the application looks for:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
~/.config/smart-linux-assistant/config.toml
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A complete example configuration is available in the repository as:
|
|
88
|
+
```
|
|
89
|
+
config.toml.example
|
|
90
|
+
```
|
|
91
|
+
Copy it into your local configuration directory and modify only the values you want to customize.
|
|
92
|
+
Example:
|
|
93
|
+
```
|
|
94
|
+
mkdir -p ~/.config/smart-linux-assistant
|
|
95
|
+
cp config.toml.example ~/.config/smart-linux-assistant/config.toml
|
|
96
|
+
```
|
|
97
|
+
Current configurable options include:
|
|
98
|
+
|
|
99
|
+
- AI provider
|
|
100
|
+
- AI model
|
|
101
|
+
- API timeout
|
|
102
|
+
- Retry count
|
|
103
|
+
- Command history settings
|
|
104
|
+
- Logging verbosity
|
|
105
|
+
- Privacy (secret redaction)
|
|
106
|
+
|
|
76
107
|
## Usage / API Reference
|
|
77
108
|
|
|
78
109
|
The project exposes the console scripts `smart-linux` and `sla` (configured in `pyproject.toml`).
|
|
@@ -193,9 +224,11 @@ Suggested fix:
|
|
|
193
224
|
|
|
194
225
|
## Privacy Note
|
|
195
226
|
|
|
196
|
-
The `explain`, `fix`, and `search` commands send
|
|
227
|
+
The `explain`, `fix`, and `search` commands send your terminal queries to Groq's API for processing.
|
|
228
|
+
|
|
229
|
+
**Important update starting in v0.7.0+:** To provide context-aware solutions, the `fix` command securely reads your 5 most recent local commands and injects them into the Groq API prompt.
|
|
197
230
|
|
|
198
|
-
Security First
|
|
231
|
+
**Security First:** Before any history leaves your machine, it passes through a local Regex Redactor (`linux_assistant.utils.redactor`) which automatically scrubs standard environment variables, inline passwords, AWS keys, and Bearer tokens, replacing them with `[REDACTED]`. However, you should still exercise caution and avoid running AI commands immediately after working with highly sensitive, non-standard plaintext secrets.
|
|
199
232
|
|
|
200
233
|
Separately, `smart-linux run` records your command invocations (command text, exit code, duration, working directory, and truncated stderr) in a local SQLite database. `stdout` is never recorded. To disable history recording entirely, set `SMART_LINUX_NO_HISTORY=1`. To view or erase recorded history, use `smart-linux history` and `smart-linux history clear`.
|
|
201
234
|
|
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
8
|
import typer
|
|
9
|
+
import re
|
|
9
10
|
from linux_assistant.exceptions import CommandExecutionError, CommandFailedError, CommandTimeoutError, HistoryError, ValidationError, MissingAPIKeyError, ServiceError, RateLimitError
|
|
10
11
|
from linux_assistant.services.explainer import Explainer
|
|
11
12
|
from linux_assistant.services.command_executor import CommandExecutor
|
|
@@ -322,8 +323,24 @@ def search(
|
|
|
322
323
|
|
|
323
324
|
typer.echo(result)
|
|
324
325
|
|
|
325
|
-
|
|
326
|
-
|
|
326
|
+
typer.echo(result)
|
|
327
|
+
|
|
328
|
+
# Extract the command from the AI's markdown output to trigger the agentic prompt
|
|
329
|
+
# First, try to find a markdown code block (```bash ... ```)
|
|
330
|
+
match = re.search(r'```(?:bash|sh|shell)?\n(.*?)\n```', result, re.DOTALL)
|
|
331
|
+
extracted_command = None
|
|
332
|
+
|
|
333
|
+
if match:
|
|
334
|
+
extracted_command = match.group(1).strip()
|
|
335
|
+
else:
|
|
336
|
+
# Fallback: look for a line starting with a shell prompt ($)
|
|
337
|
+
fallback_match = re.search(r'(?:^|\n)(?:(?:\$|#)\s+)?([a-zA-Z0-9_].*)', result)
|
|
338
|
+
if fallback_match:
|
|
339
|
+
extracted_command = fallback_match.group(1).strip()
|
|
340
|
+
extracted_command = extracted_command.replace("`", "") # Strip stray backticks
|
|
341
|
+
|
|
342
|
+
if extracted_command:
|
|
343
|
+
_prompt_and_execute(extracted_command)
|
|
327
344
|
|
|
328
345
|
|
|
329
346
|
history_app = typer.Typer(help="View and manage recorded command history.")
|
|
@@ -418,7 +435,14 @@ def _prompt_and_execute(command: str) -> None:
|
|
|
418
435
|
typer.secho(f"Executing: {command}\n", fg=typer.colors.CYAN)
|
|
419
436
|
|
|
420
437
|
executor = CommandExecutor()
|
|
421
|
-
|
|
438
|
+
try:
|
|
439
|
+
result = executor.execute(command)
|
|
440
|
+
except CommandTimeoutError as exc:
|
|
441
|
+
typer.secho(f"Timed out: {exc}", fg=typer.colors.RED, err=True)
|
|
442
|
+
raise typer.Exit(code=124)
|
|
443
|
+
except CommandExecutionError as exc:
|
|
444
|
+
typer.secho(f"Execution error: {exc}", fg=typer.colors.RED, err=True)
|
|
445
|
+
raise typer.Exit(code=1)
|
|
422
446
|
|
|
423
447
|
if result.succeeded:
|
|
424
448
|
if result.stdout:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from linux_assistant.config.models import (
|
|
4
|
+
AIConfig,
|
|
5
|
+
AppConfig,
|
|
6
|
+
HistoryConfig,
|
|
7
|
+
LoggingConfig,
|
|
8
|
+
PrivacyConfig,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_AI_CONFIG = AIConfig(
|
|
13
|
+
provider="groq",
|
|
14
|
+
model="llama-3.3-70b-versatile",
|
|
15
|
+
timeout_seconds=30.0,
|
|
16
|
+
max_retries=3,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DEFAULT_HISTORY_CONFIG = HistoryConfig(
|
|
20
|
+
enabled=True,
|
|
21
|
+
max_entries=5000,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
DEFAULT_LOGGING_CONFIG = LoggingConfig(
|
|
25
|
+
verbose=False,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
DEFAULT_PRIVACY_CONFIG = PrivacyConfig(
|
|
29
|
+
redaction_enabled=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
DEFAULT_CONFIG = AppConfig(
|
|
33
|
+
ai=DEFAULT_AI_CONFIG,
|
|
34
|
+
history=DEFAULT_HISTORY_CONFIG,
|
|
35
|
+
logging=DEFAULT_LOGGING_CONFIG,
|
|
36
|
+
privacy=DEFAULT_PRIVACY_CONFIG,
|
|
37
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tomllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from linux_assistant.config.defaults import DEFAULT_CONFIG
|
|
8
|
+
from linux_assistant.config.models import AppConfig
|
|
9
|
+
from linux_assistant.config.validator import validate_config
|
|
10
|
+
from linux_assistant.exceptions import ConfigurationError
|
|
11
|
+
|
|
12
|
+
CONFIG_FILE_NAME = "config.toml"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_config_path() -> Path:
|
|
16
|
+
"""
|
|
17
|
+
Return the location of the user configuration file.
|
|
18
|
+
|
|
19
|
+
The configuration file follows the XDG Base Directory specification.
|
|
20
|
+
If XDG_CONFIG_HOME is not set, ~/.config is used.
|
|
21
|
+
"""
|
|
22
|
+
xdg_config_home = os.getenv("XDG_CONFIG_HOME")
|
|
23
|
+
|
|
24
|
+
if xdg_config_home:
|
|
25
|
+
config_home = Path(xdg_config_home)
|
|
26
|
+
else:
|
|
27
|
+
config_home = Path.home() / ".config"
|
|
28
|
+
|
|
29
|
+
return config_home / "smart-linux-assistant" / CONFIG_FILE_NAME
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_config() -> AppConfig:
|
|
33
|
+
"""
|
|
34
|
+
Load the application configuration.
|
|
35
|
+
|
|
36
|
+
Returns the default configuration if no configuration file exists.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ConfigurationError:
|
|
40
|
+
If the configuration file cannot be read or contains
|
|
41
|
+
invalid TOML or invalid configuration values.
|
|
42
|
+
"""
|
|
43
|
+
config_path = get_config_path()
|
|
44
|
+
|
|
45
|
+
if not config_path.exists():
|
|
46
|
+
return DEFAULT_CONFIG
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
with config_path.open("rb") as file:
|
|
50
|
+
data = tomllib.load(file)
|
|
51
|
+
|
|
52
|
+
except tomllib.TOMLDecodeError as exc:
|
|
53
|
+
raise ConfigurationError(
|
|
54
|
+
f"Invalid TOML syntax in '{config_path}'."
|
|
55
|
+
) from exc
|
|
56
|
+
|
|
57
|
+
except OSError as exc:
|
|
58
|
+
raise ConfigurationError(
|
|
59
|
+
f"Unable to read configuration file '{config_path}'."
|
|
60
|
+
) from exc
|
|
61
|
+
|
|
62
|
+
print(f"Using config: {config_path}")
|
|
63
|
+
print(f"Model: {data['ai']['model']}")
|
|
64
|
+
return validate_config(data)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class PrivacyConfig:
|
|
8
|
+
"""
|
|
9
|
+
Privacy-related configuration.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
redaction_enabled: bool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class LoggingConfig:
|
|
17
|
+
"""
|
|
18
|
+
Logging-related configuration.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
verbose: bool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class HistoryConfig:
|
|
26
|
+
"""
|
|
27
|
+
History-related configuration.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
enabled: bool
|
|
31
|
+
max_entries: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class AIConfig:
|
|
36
|
+
"""
|
|
37
|
+
AI provider configuration.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
provider: str
|
|
41
|
+
model: str
|
|
42
|
+
timeout_seconds: float
|
|
43
|
+
max_retries: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class AppConfig:
|
|
48
|
+
"""
|
|
49
|
+
Root application configuration.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
ai: AIConfig
|
|
53
|
+
history: HistoryConfig
|
|
54
|
+
logging: LoggingConfig
|
|
55
|
+
privacy: PrivacyConfig
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from linux_assistant.config.defaults import DEFAULT_CONFIG
|
|
6
|
+
from linux_assistant.config.models import (
|
|
7
|
+
AIConfig,
|
|
8
|
+
AppConfig,
|
|
9
|
+
HistoryConfig,
|
|
10
|
+
LoggingConfig,
|
|
11
|
+
PrivacyConfig,
|
|
12
|
+
)
|
|
13
|
+
from linux_assistant.exceptions import ConfigurationError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_config(data: dict[str, Any]) -> AppConfig:
|
|
17
|
+
"""
|
|
18
|
+
Validate and normalize a parsed TOML configuration.
|
|
19
|
+
Missing sections and fields fall back to DEFAULT_CONFIG.
|
|
20
|
+
Invalid field types raise ConfigurationError.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
if not isinstance(data, dict):
|
|
24
|
+
raise ConfigurationError(
|
|
25
|
+
"Configuration root must be a TOML table."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
ai = data.get("ai", {})
|
|
29
|
+
history = data.get("history", {})
|
|
30
|
+
logging = data.get("logging", {})
|
|
31
|
+
privacy = data.get("privacy", {})
|
|
32
|
+
|
|
33
|
+
_require_table(ai, "ai")
|
|
34
|
+
_require_table(history, "history")
|
|
35
|
+
_require_table(logging, "logging")
|
|
36
|
+
_require_table(privacy, "privacy")
|
|
37
|
+
|
|
38
|
+
return AppConfig(
|
|
39
|
+
ai=AIConfig(
|
|
40
|
+
provider=_get_str(
|
|
41
|
+
ai,
|
|
42
|
+
"provider",
|
|
43
|
+
DEFAULT_CONFIG.ai.provider,
|
|
44
|
+
),
|
|
45
|
+
model=_get_str(
|
|
46
|
+
ai,
|
|
47
|
+
"model",
|
|
48
|
+
DEFAULT_CONFIG.ai.model,
|
|
49
|
+
),
|
|
50
|
+
timeout_seconds=_get_float(
|
|
51
|
+
ai,
|
|
52
|
+
"timeout_seconds",
|
|
53
|
+
DEFAULT_CONFIG.ai.timeout_seconds,
|
|
54
|
+
),
|
|
55
|
+
max_retries=_get_int(
|
|
56
|
+
ai,
|
|
57
|
+
"max_retries",
|
|
58
|
+
DEFAULT_CONFIG.ai.max_retries,
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
history=HistoryConfig(
|
|
62
|
+
enabled=_get_bool(
|
|
63
|
+
history,
|
|
64
|
+
"enabled",
|
|
65
|
+
DEFAULT_CONFIG.history.enabled,
|
|
66
|
+
),
|
|
67
|
+
max_entries=_get_int(
|
|
68
|
+
history,
|
|
69
|
+
"max_entries",
|
|
70
|
+
DEFAULT_CONFIG.history.max_entries,
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
logging=LoggingConfig(
|
|
74
|
+
verbose=_get_bool(
|
|
75
|
+
logging,
|
|
76
|
+
"verbose",
|
|
77
|
+
DEFAULT_CONFIG.logging.verbose,
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
privacy=PrivacyConfig(
|
|
81
|
+
redaction_enabled=_get_bool(
|
|
82
|
+
privacy,
|
|
83
|
+
"redaction_enabled",
|
|
84
|
+
DEFAULT_CONFIG.privacy.redaction_enabled,
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _require_table(value: Any, section: str) -> None:
|
|
91
|
+
if not isinstance(value, dict):
|
|
92
|
+
raise ConfigurationError(
|
|
93
|
+
f"Configuration section '{section}' must be a TOML table."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _get_str(
|
|
98
|
+
table: dict[str, Any],
|
|
99
|
+
key: str,
|
|
100
|
+
default: str,
|
|
101
|
+
) -> str:
|
|
102
|
+
value = table.get(key, default)
|
|
103
|
+
|
|
104
|
+
if not isinstance(value, str):
|
|
105
|
+
raise ConfigurationError(
|
|
106
|
+
f"'{key}' must be a string."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _get_int(
|
|
113
|
+
table: dict[str, Any],
|
|
114
|
+
key: str,
|
|
115
|
+
default: int,
|
|
116
|
+
) -> int:
|
|
117
|
+
value = table.get(key, default)
|
|
118
|
+
|
|
119
|
+
if not isinstance(value, int):
|
|
120
|
+
raise ConfigurationError(
|
|
121
|
+
f"'{key}' must be an integer."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _get_float(
|
|
128
|
+
table: dict[str, Any],
|
|
129
|
+
key: str,
|
|
130
|
+
default: float,
|
|
131
|
+
) -> float:
|
|
132
|
+
value = table.get(key, default)
|
|
133
|
+
|
|
134
|
+
if not isinstance(value, (int, float)):
|
|
135
|
+
raise ConfigurationError(
|
|
136
|
+
f"'{key}' must be a number."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return float(value)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _get_bool(
|
|
143
|
+
table: dict[str, Any],
|
|
144
|
+
key: str,
|
|
145
|
+
default: bool,
|
|
146
|
+
) -> bool:
|
|
147
|
+
value = table.get(key, default)
|
|
148
|
+
|
|
149
|
+
if not isinstance(value, bool):
|
|
150
|
+
raise ConfigurationError(
|
|
151
|
+
f"'{key}' must be a boolean."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return value
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/explainer.py
RENAMED
|
@@ -4,8 +4,11 @@ AI-powered explanations for Linux commands and error messages.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
import groq
|
|
7
|
-
import
|
|
8
|
-
|
|
7
|
+
from linux_assistant.utils.groq_client import (
|
|
8
|
+
GROQ_MODEL,
|
|
9
|
+
build_groq_client,
|
|
10
|
+
truncate_for_api,
|
|
11
|
+
)
|
|
9
12
|
from linux_assistant.exceptions import MissingAPIKeyError, ServiceError, ValidationError, RateLimitError, HistoryError
|
|
10
13
|
from linux_assistant.utils.logger import get_logger
|
|
11
14
|
from linux_assistant.repositories.history_repository import HistoryRepository
|
|
@@ -122,11 +125,11 @@ class Explainer:
|
|
|
122
125
|
scrubbed_lines.append(f"[{status}] {scrubbed_cmd}")
|
|
123
126
|
|
|
124
127
|
history_context = "\n".join(scrubbed_lines)
|
|
125
|
-
except HistoryError:
|
|
126
|
-
# GRACEFUL DEGRADATION:
|
|
127
|
-
|
|
128
|
-
"
|
|
129
|
-
|
|
128
|
+
except HistoryError as exc:
|
|
129
|
+
# GRACEFUL DEGRADATION: Log the failure but do not crash or print to stdout directly.
|
|
130
|
+
logger.warning(
|
|
131
|
+
"Could not read local history (database locked). Falling back to generic explanation. Error: %s",
|
|
132
|
+
exc
|
|
130
133
|
)
|
|
131
134
|
|
|
132
135
|
# Inject into the System Prompt
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/search.py
RENAMED
|
@@ -5,10 +5,12 @@ AI-powered natural-language search for Linux commands and tasks.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
import groq
|
|
7
7
|
from linux_assistant.exceptions import ServiceError, ValidationError, RateLimitError
|
|
8
|
-
from linux_assistant.utils.groq_client import
|
|
8
|
+
from linux_assistant.utils.groq_client import (
|
|
9
|
+
GROQ_MODEL,
|
|
10
|
+
build_groq_client,
|
|
11
|
+
truncate_for_api,
|
|
12
|
+
)
|
|
9
13
|
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
14
|
logger = get_logger(__name__)
|
|
13
15
|
|
|
14
16
|
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.
|
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/utils/groq_client.py
RENAMED
|
@@ -5,14 +5,14 @@ Shared Groq client construction for AI-powered services.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
import os
|
|
7
7
|
from groq import Groq
|
|
8
|
+
from linux_assistant.config.loader import load_config
|
|
8
9
|
from linux_assistant.exceptions import MissingAPIKeyError
|
|
9
10
|
|
|
10
11
|
GROQ_API_KEY = "GROQ_API_KEY"
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
CONFIG = load_config()
|
|
13
|
+
GROQ_MODEL = CONFIG.ai.model
|
|
13
14
|
MAX_INPUT_CHARACTERS = 4000
|
|
14
|
-
|
|
15
|
-
MAX_RETRIES = 2
|
|
15
|
+
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def build_groq_client() -> Groq:
|
|
@@ -34,8 +34,8 @@ def build_groq_client() -> Groq:
|
|
|
34
34
|
|
|
35
35
|
return Groq(
|
|
36
36
|
api_key=api_key,
|
|
37
|
-
timeout=
|
|
38
|
-
max_retries=
|
|
37
|
+
timeout=CONFIG.ai.timeout_seconds,
|
|
38
|
+
max_retries=CONFIG.ai.max_retries,
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
def truncate_for_api(text: str, *, keep_end: bool = False) -> str:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redacts sensitive information from shell commands and stderr before API transmission.
|
|
3
|
+
"""
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
# Common patterns for secrets in CLI usage
|
|
7
|
+
SECRET_PATTERNS = [
|
|
8
|
+
# Bearer tokens: Authorization: Bearer <token>
|
|
9
|
+
(r'(?i)(bearer\s+)[\w\-\.\~]+', r'\1[REDACTED]'),
|
|
10
|
+
|
|
11
|
+
# Environment variables or standard flags: API_KEY=xyz, DB_PASS="abc", --password="123"
|
|
12
|
+
# -> Added 'pass' and 'pwd' to the keyword capture group
|
|
13
|
+
(r'(?i)(api_?key|password|pass|pwd|secret|token|auth_?token)(\s*=\s*["\']?)[\w\-\.\~]+(["\']?)', r'\1\2[REDACTED]\3'),
|
|
14
|
+
|
|
15
|
+
# Inline password flags: mysql -pMySecret (no space) or --password abc
|
|
16
|
+
# -> Specifically looks for -p immediately followed by characters to avoid redacting ports (e.g. ssh -p 22)
|
|
17
|
+
(r'(?i)(\s-p(?!\s)|\-\-password\s*=?\s*)[\w\-\.\~]+', r'\1[REDACTED]'),
|
|
18
|
+
|
|
19
|
+
# AWS-style keys
|
|
20
|
+
(r'(?i)(AKIA[0-9A-Z]{16})', r'[REDACTED_AWS_KEY]'),
|
|
21
|
+
|
|
22
|
+
# Passwords in URLs: https://user:pass@host.com
|
|
23
|
+
(r'(https?://[^:]+:)([^@]+)(@)', r'\1[REDACTED]\3')
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
def scrub_secrets(text: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Applies regex patterns to redact potential secrets from text.
|
|
29
|
+
"""
|
|
30
|
+
if not text:
|
|
31
|
+
return text
|
|
32
|
+
|
|
33
|
+
redacted_text = text
|
|
34
|
+
for pattern, replacement in SECRET_PATTERNS:
|
|
35
|
+
redacted_text = re.sub(pattern, replacement, redacted_text)
|
|
36
|
+
|
|
37
|
+
return redacted_text
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "smart-linux-assistant"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.2"
|
|
8
8
|
description = "An AI-powered Linux productivity assistant for command analysis, troubleshooting, and knowledge management."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2/smart_linux_assistant.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smart-linux-assistant
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: An AI-powered Linux productivity assistant for command analysis, troubleshooting, and knowledge management.
|
|
5
5
|
Author: Shubham Kumar Jha
|
|
6
6
|
License: MIT
|
|
@@ -100,6 +100,37 @@ This project does not require any environment variables for its core CLI functio
|
|
|
100
100
|
| --- | --- | --- |
|
|
101
101
|
| (none) | No required environment variables for CLI execution | - |
|
|
102
102
|
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
Smart Linux Assistant supports a user configuration file. If no configuration file is present, the application automatically falls back to built-in defaults.
|
|
107
|
+
|
|
108
|
+
By default, the application looks for:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
~/.config/smart-linux-assistant/config.toml
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A complete example configuration is available in the repository as:
|
|
115
|
+
```
|
|
116
|
+
config.toml.example
|
|
117
|
+
```
|
|
118
|
+
Copy it into your local configuration directory and modify only the values you want to customize.
|
|
119
|
+
Example:
|
|
120
|
+
```
|
|
121
|
+
mkdir -p ~/.config/smart-linux-assistant
|
|
122
|
+
cp config.toml.example ~/.config/smart-linux-assistant/config.toml
|
|
123
|
+
```
|
|
124
|
+
Current configurable options include:
|
|
125
|
+
|
|
126
|
+
- AI provider
|
|
127
|
+
- AI model
|
|
128
|
+
- API timeout
|
|
129
|
+
- Retry count
|
|
130
|
+
- Command history settings
|
|
131
|
+
- Logging verbosity
|
|
132
|
+
- Privacy (secret redaction)
|
|
133
|
+
|
|
103
134
|
## Usage / API Reference
|
|
104
135
|
|
|
105
136
|
The project exposes the console scripts `smart-linux` and `sla` (configured in `pyproject.toml`).
|
|
@@ -220,9 +251,11 @@ Suggested fix:
|
|
|
220
251
|
|
|
221
252
|
## Privacy Note
|
|
222
253
|
|
|
223
|
-
The `explain`, `fix`, and `search` commands send
|
|
254
|
+
The `explain`, `fix`, and `search` commands send your terminal queries to Groq's API for processing.
|
|
255
|
+
|
|
256
|
+
**Important update starting in v0.7.0+:** To provide context-aware solutions, the `fix` command securely reads your 5 most recent local commands and injects them into the Groq API prompt.
|
|
224
257
|
|
|
225
|
-
Security First
|
|
258
|
+
**Security First:** Before any history leaves your machine, it passes through a local Regex Redactor (`linux_assistant.utils.redactor`) which automatically scrubs standard environment variables, inline passwords, AWS keys, and Bearer tokens, replacing them with `[REDACTED]`. However, you should still exercise caution and avoid running AI commands immediately after working with highly sensitive, non-standard plaintext secrets.
|
|
226
259
|
|
|
227
260
|
Separately, `smart-linux run` records your command invocations (command text, exit code, duration, working directory, and truncated stderr) in a local SQLite database. `stdout` is never recorded. To disable history recording entirely, set `SMART_LINUX_NO_HISTORY=1`. To view or erase recorded history, use `smart-linux history` and `smart-linux history clear`.
|
|
228
261
|
|
|
@@ -5,7 +5,12 @@ linux_assistant/__init__.py
|
|
|
5
5
|
linux_assistant/cli/__init__.py
|
|
6
6
|
linux_assistant/cli/main.py
|
|
7
7
|
linux_assistant/config/__init__.py
|
|
8
|
+
linux_assistant/config/defaults.py
|
|
9
|
+
linux_assistant/config/exceptions.py
|
|
10
|
+
linux_assistant/config/loader.py
|
|
11
|
+
linux_assistant/config/models.py
|
|
8
12
|
linux_assistant/config/settings.py
|
|
13
|
+
linux_assistant/config/validator.py
|
|
9
14
|
linux_assistant/core/__init__.py
|
|
10
15
|
linux_assistant/core/safety.py
|
|
11
16
|
linux_assistant/exceptions/__init__.py
|
|
@@ -32,6 +37,7 @@ smart_linux_assistant.egg-info/requires.txt
|
|
|
32
37
|
smart_linux_assistant.egg-info/top_level.txt
|
|
33
38
|
tests/test_cli.py
|
|
34
39
|
tests/test_command_executor.py
|
|
40
|
+
tests/test_config_validator.py
|
|
35
41
|
tests/test_explainer.py
|
|
36
42
|
tests/test_groq_client.py
|
|
37
43
|
tests/test_history_repository.py
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from linux_assistant.config.defaults import DEFAULT_CONFIG
|
|
4
|
+
from linux_assistant.config.validator import validate_config
|
|
5
|
+
from linux_assistant.exceptions import ConfigurationError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_empty_configuration_returns_defaults() -> None:
|
|
9
|
+
config = validate_config({})
|
|
10
|
+
|
|
11
|
+
assert config == DEFAULT_CONFIG
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_partial_configuration_overrides_defaults() -> None:
|
|
15
|
+
config = validate_config(
|
|
16
|
+
{
|
|
17
|
+
"logging": {
|
|
18
|
+
"verbose": True,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
assert config.logging.verbose is True
|
|
24
|
+
assert config.ai == DEFAULT_CONFIG.ai
|
|
25
|
+
assert config.history == DEFAULT_CONFIG.history
|
|
26
|
+
assert config.privacy == DEFAULT_CONFIG.privacy
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_invalid_boolean_raises_configuration_error() -> None:
|
|
30
|
+
with pytest.raises(ConfigurationError):
|
|
31
|
+
validate_config(
|
|
32
|
+
{
|
|
33
|
+
"logging": {
|
|
34
|
+
"verbose": "yes",
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_invalid_integer_raises_configuration_error() -> None:
|
|
41
|
+
with pytest.raises(ConfigurationError):
|
|
42
|
+
validate_config(
|
|
43
|
+
{
|
|
44
|
+
"history": {
|
|
45
|
+
"max_entries": "5000",
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_invalid_section_type_raises_configuration_error() -> None:
|
|
52
|
+
with pytest.raises(ConfigurationError):
|
|
53
|
+
validate_config(
|
|
54
|
+
{
|
|
55
|
+
"history": True,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
def scrub_secrets(command: str) -> str:
|
|
4
|
-
"""
|
|
5
|
-
Scans a raw terminal command and replaces sensitive API keys, passwords, tokens with [REDACTED] to ensure safe LLM transit.
|
|
6
|
-
"""
|
|
7
|
-
command = re.sub(
|
|
8
|
-
r'(?i)([A-Z0-9_]*(?:KEY|TOKEN|PASSWORD|SECRET|PASS)[A-Z0-9_]*\s*=\s*["\']?)[^"\'\s]+(["\']?)',
|
|
9
|
-
r'\g<1>[REDACTED]\g<2>',
|
|
10
|
-
command
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
command = re.sub(
|
|
14
|
-
r'(-p|--password=?)\s*(["\']?)[^"\'\s]+(["\']?)',
|
|
15
|
-
r'\1\2[REDACTED]\3',
|
|
16
|
-
command
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
command = re.sub(
|
|
20
|
-
r'([Bb]earer\s+)(["\']?)[^"\'\s]+(["\']?)',
|
|
21
|
-
r'\1\2[REDACTED]\3',
|
|
22
|
-
command
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
return command
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/config/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/config/settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/exceptions/__init__.py
RENAMED
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/exceptions/base.py
RENAMED
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/models/__init__.py
RENAMED
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/models/command_result.py
RENAMED
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/models/history_entry.py
RENAMED
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/repositories/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_history_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|