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.
Files changed (50) hide show
  1. {smart_linux_assistant-0.8.0/smart_linux_assistant.egg-info → smart_linux_assistant-0.8.2}/PKG-INFO +36 -3
  2. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/README.md +35 -2
  3. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/cli/main.py +27 -3
  4. smart_linux_assistant-0.8.2/linux_assistant/config/defaults.py +37 -0
  5. smart_linux_assistant-0.8.2/linux_assistant/config/loader.py +64 -0
  6. smart_linux_assistant-0.8.2/linux_assistant/config/models.py +55 -0
  7. smart_linux_assistant-0.8.2/linux_assistant/config/validator.py +154 -0
  8. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/explainer.py +10 -7
  9. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/search.py +5 -3
  10. smart_linux_assistant-0.8.2/linux_assistant/utils/__init__.py +0 -0
  11. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/utils/groq_client.py +6 -6
  12. smart_linux_assistant-0.8.2/linux_assistant/utils/redactor.py +37 -0
  13. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/pyproject.toml +1 -1
  14. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2/smart_linux_assistant.egg-info}/PKG-INFO +36 -3
  15. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/smart_linux_assistant.egg-info/SOURCES.txt +6 -0
  16. smart_linux_assistant-0.8.2/tests/test_config_validator.py +57 -0
  17. smart_linux_assistant-0.8.0/linux_assistant/utils/redactor.py +0 -25
  18. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/LICENSE +0 -0
  19. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/__init__.py +0 -0
  20. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/cli/__init__.py +0 -0
  21. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/config/__init__.py +0 -0
  22. /smart_linux_assistant-0.8.0/linux_assistant/core/__init__.py → /smart_linux_assistant-0.8.2/linux_assistant/config/exceptions.py +0 -0
  23. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/config/settings.py +0 -0
  24. {smart_linux_assistant-0.8.0/linux_assistant/utils → smart_linux_assistant-0.8.2/linux_assistant/core}/__init__.py +0 -0
  25. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/core/safety.py +0 -0
  26. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/exceptions/__init__.py +0 -0
  27. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/exceptions/base.py +0 -0
  28. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/models/__init__.py +0 -0
  29. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/models/command_result.py +0 -0
  30. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/models/history_entry.py +0 -0
  31. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/repositories/__init__.py +0 -0
  32. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/repositories/history_repository.py +0 -0
  33. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/__init__.py +0 -0
  34. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/services/command_executor.py +0 -0
  35. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/utils/logger.py +0 -0
  36. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/linux_assistant/utils/shell.py +0 -0
  37. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/setup.cfg +0 -0
  38. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/smart_linux_assistant.egg-info/dependency_links.txt +0 -0
  39. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/smart_linux_assistant.egg-info/entry_points.txt +0 -0
  40. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/smart_linux_assistant.egg-info/requires.txt +0 -0
  41. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/smart_linux_assistant.egg-info/top_level.txt +0 -0
  42. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_cli.py +0 -0
  43. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_command_executor.py +0 -0
  44. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_explainer.py +0 -0
  45. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_groq_client.py +0 -0
  46. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_history_repository.py +0 -0
  47. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_redactor.py +0 -0
  48. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_safety.py +0 -0
  49. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_search.py +0 -0
  50. {smart_linux_assistant-0.8.0 → smart_linux_assistant-0.8.2}/tests/test_shell.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smart-linux-assistant
3
- Version: 0.8.0
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 the command text, error output, or your query to Groq's API for processing. The explain, fix, and search commands send your terminal queries to Groq's API for processing. Starting in v0.7.0, `explain` and `fix` also securely read your recent local command history to provide context-aware solutions.
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: 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, and Bearer tokens, replacing them with [REDACTED]. However, you should still exercise caution and avoid running these commands on highly sensitive plaintext data.
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 the command text, error output, or your query to Groq's API for processing. The explain, fix, and search commands send your terminal queries to Groq's API for processing. Starting in v0.7.0, `explain` and `fix` also securely read your recent local command history to provide context-aware solutions.
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: 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, and Bearer tokens, replacing them with [REDACTED]. However, you should still exercise caution and avoid running these commands on highly sensitive plaintext data.
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
- if hasattr(result, "command") and result.command:
326
- _prompt_and_execute(result.command)
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
- result = executor.execute(command)
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
@@ -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 typer
8
- from linux_assistant.utils.groq_client import GROQ_MODEL, build_groq_client, truncate_for_api
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: Print a warning, but don't crash
127
- typer.secho(
128
- "⚠ Warning: Could not read local history (database locked). Falling back to generic explanation.",
129
- fg=typer.colors.YELLOW
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
@@ -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 GROQ_MODEL, build_groq_client
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.
@@ -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
- GROQ_MODEL = "llama-3.3-70b-versatile"
12
-
12
+ CONFIG = load_config()
13
+ GROQ_MODEL = CONFIG.ai.model
13
14
  MAX_INPUT_CHARACTERS = 4000
14
- REQUEST_TIMEOUT_SECONDS = 30.0
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=REQUEST_TIMEOUT_SECONDS,
38
- max_retries=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.0"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smart-linux-assistant
3
- Version: 0.8.0
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 the command text, error output, or your query to Groq's API for processing. The explain, fix, and search commands send your terminal queries to Groq's API for processing. Starting in v0.7.0, `explain` and `fix` also securely read your recent local command history to provide context-aware solutions.
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: 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, and Bearer tokens, replacing them with [REDACTED]. However, you should still exercise caution and avoid running these commands on highly sensitive plaintext data.
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