naturally-linux 0.1.0__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.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: naturally-linux
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: groq>=1.0.0
8
+ Requires-Dist: pytest>=9.0.2
9
+ Requires-Dist: python-dotenv>=1.2.1
10
+ Requires-Dist: typer>=0.21.1
File without changes
@@ -0,0 +1,4 @@
1
+ """Naturally Linux package initializer."""
2
+
3
+ # Expose the Typer app at package level if needed by external callers.
4
+ from .cli import app # noqa: F401
@@ -0,0 +1,12 @@
1
+ """Run the Typer app with python -m naturally_linux."""
2
+
3
+ from .cli import app
4
+
5
+
6
+ def main() -> None:
7
+ # Ensure help shows the intended CLI name when invoked via python -m.
8
+ app(prog_name="naturally-linux")
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,91 @@
1
+ """Typer CLI entrypoint for Naturally Linux."""
2
+
3
+ import typer
4
+
5
+ from .executor import run_command
6
+ from .generator import explain_command, generate_command
7
+ from .safety import is_safe_command
8
+
9
+ # Create the Typer app instance.
10
+ # This object registers and groups all CLI commands.
11
+ app = typer.Typer(
12
+ help="Naturally Linux: run shell tasks using natural language.")
13
+
14
+
15
+ @app.command()
16
+ def run(
17
+ prompt: str = typer.Argument(...,
18
+ help="Natural language task description."),
19
+ auto_approve: bool = typer.Option(
20
+ False, "--yes", "-y", help="Auto-approve without confirmation."
21
+ ),
22
+ dry_run: bool = typer.Option(
23
+ False, "--dry-run", help="Preview command without executing it."
24
+ ),
25
+ ):
26
+ """
27
+ Convert a natural-language instruction into a shell command,
28
+ display it, and optionally execute it.
29
+ """
30
+
31
+ # Step 1: Ask the generator to produce a shell command.
32
+ command = generate_command(prompt)
33
+
34
+ # Step 2: Run safety checks on the generated command.
35
+ if not is_safe_command(command):
36
+ typer.secho("Potentially unsafe command detected.",
37
+ fg=typer.colors.RED)
38
+ if not auto_approve and not typer.confirm("Proceed anyway?", default=False):
39
+ raise typer.Abort()
40
+
41
+ # Step 3: Show the user the proposed command.
42
+ typer.secho("Proposed command:", fg=typer.colors.CYAN)
43
+ typer.echo(command)
44
+
45
+ # Step 4: If dry-run, explain the command and optionally execute.
46
+ if dry_run:
47
+ try:
48
+ explanation = explain_command(command)
49
+ if explanation:
50
+ typer.secho("\nExplanation:", fg=typer.colors.GREEN)
51
+ typer.echo(explanation)
52
+ except RuntimeError as exc:
53
+ typer.secho(str(exc), fg=typer.colors.YELLOW)
54
+
55
+ safety_label = "SAFE" if is_safe_command(command) else "UNSAFE"
56
+ safety_color = typer.colors.GREEN if safety_label == "SAFE" else typer.colors.RED
57
+ typer.secho(f"\nSafety check: {safety_label}", fg=safety_color)
58
+
59
+ if auto_approve or typer.confirm("Run this command now?", default=False):
60
+ stdout, stderr, returncode = run_command(command)
61
+
62
+ if stdout:
63
+ typer.echo(stdout)
64
+
65
+ if stderr:
66
+ typer.secho(stderr, fg=typer.colors.YELLOW)
67
+
68
+ if returncode != 0:
69
+ raise typer.Exit(code=returncode)
70
+
71
+ typer.secho(
72
+ "\nDry run mode — command not executed.",
73
+ fg=typer.colors.YELLOW,
74
+ )
75
+ return
76
+
77
+ # Step 5: Ask for confirmation unless auto-approved.
78
+ if not auto_approve and not typer.confirm("Run this command?", default=False):
79
+ raise typer.Abort()
80
+
81
+ # Step 6: Execute the command and surface output.
82
+ stdout, stderr, returncode = run_command(command)
83
+
84
+ if stdout:
85
+ typer.echo(stdout)
86
+
87
+ if stderr:
88
+ typer.secho(stderr, fg=typer.colors.YELLOW)
89
+
90
+ if returncode != 0:
91
+ raise typer.Exit(code=returncode)
@@ -0,0 +1,23 @@
1
+ """Command execution helpers."""
2
+
3
+ import subprocess
4
+ from typing import Tuple
5
+
6
+
7
+ def run_command(command: str) -> Tuple[str, str, int]:
8
+ """
9
+ Execute a shell command and return (stdout, stderr, returncode).
10
+
11
+ TODO:
12
+ - Consider a safer execution strategy (no shell, explicit args).
13
+ - Add timeouts and resource limits.
14
+ """
15
+
16
+ result = subprocess.run(
17
+ command,
18
+ shell=True,
19
+ text=True,
20
+ capture_output=True,
21
+ )
22
+
23
+ return result.stdout, result.stderr, result.returncode
@@ -0,0 +1,78 @@
1
+ """Prompt → command generator (Groq LLM integration)."""
2
+
3
+ import os
4
+
5
+ from dotenv import load_dotenv
6
+ from groq import Groq
7
+
8
+ load_dotenv()
9
+
10
+ SYSTEM_PROMPT = (
11
+ "You are a Linux command generator. "
12
+ "Return ONLY the exact shell command to accomplish the user's task. "
13
+ "Do NOT include explanations, markdown, code fences, or extra text. "
14
+ "If multiple commands are required, chain them with &&. "
15
+ "Assume a POSIX shell."
16
+ )
17
+
18
+ EXPLAIN_PROMPT = (
19
+ "You are a Linux command explainer. "
20
+ "Explain succinctly what the command does and any notable risks. "
21
+ "Do NOT include markdown, code fences, or extra text beyond the explanation."
22
+ )
23
+
24
+
25
+ def generate_command(prompt: str) -> str:
26
+ """
27
+ Generate a shell command from a natural language prompt using Groq.
28
+
29
+ Requires the GROQ_API_KEY environment variable to be set.
30
+ """
31
+
32
+ api_key = os.environ.get("GROQ_API_KEY")
33
+ if not api_key:
34
+ raise RuntimeError("GROQ_API_KEY is not set")
35
+
36
+ client = Groq(api_key=api_key)
37
+
38
+ chat_completion = client.chat.completions.create(
39
+ model="llama-3.3-70b-versatile",
40
+ messages=[
41
+ {"role": "system", "content": SYSTEM_PROMPT},
42
+ {"role": "user", "content": prompt},
43
+ ],
44
+ temperature=0.2,
45
+ max_tokens=200,
46
+ )
47
+
48
+ content = (chat_completion.choices[0].message.content or "").strip()
49
+
50
+ # Ensure we return only a single command line.
51
+ return content.splitlines()[0].strip()
52
+
53
+
54
+ def explain_command(command: str) -> str:
55
+ """
56
+ Explain a shell command in plain language using Groq.
57
+
58
+ Requires the GROQ_API_KEY environment variable to be set.
59
+ """
60
+
61
+ api_key = os.environ.get("GROQ_API_KEY")
62
+ if not api_key:
63
+ raise RuntimeError("GROQ_API_KEY is not set")
64
+
65
+ client = Groq(api_key=api_key)
66
+
67
+ chat_completion = client.chat.completions.create(
68
+ model="llama-3.3-70b-versatile",
69
+ messages=[
70
+ {"role": "system", "content": EXPLAIN_PROMPT},
71
+ {"role": "user", "content": command},
72
+ ],
73
+ temperature=0.2,
74
+ max_tokens=200,
75
+ )
76
+
77
+ content = (chat_completion.choices[0].message.content or "").strip()
78
+ return content
@@ -0,0 +1,29 @@
1
+ """Safety checks for generated shell commands."""
2
+
3
+ from typing import Iterable
4
+
5
+
6
+ def _dangerous_patterns() -> Iterable[str]:
7
+ """
8
+ Return a small blacklist of patterns that should require explicit approval.
9
+
10
+ TODO:
11
+ - Replace with a richer ruleset (regex, parsing, allow/deny lists).
12
+ - Add OS-specific checks and context-aware risk assessment.
13
+ """
14
+
15
+ return [
16
+ "rm -rf", # recursive delete
17
+ "mkfs", # format filesystem
18
+ "dd if=", # raw disk writes
19
+ ":(){", # fork bomb
20
+ "shutdown",
21
+ "reboot",
22
+ ]
23
+
24
+
25
+ def is_safe_command(command: str) -> bool:
26
+ """Return True if the command appears safe, False otherwise."""
27
+
28
+ lowered = command.lower()
29
+ return not any(pattern in lowered for pattern in _dangerous_patterns())
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: naturally-linux
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: groq>=1.0.0
8
+ Requires-Dist: pytest>=9.0.2
9
+ Requires-Dist: python-dotenv>=1.2.1
10
+ Requires-Dist: typer>=0.21.1
@@ -0,0 +1,17 @@
1
+ README.md
2
+ pyproject.toml
3
+ naturally_linux/__init__.py
4
+ naturally_linux/__main__.py
5
+ naturally_linux/cli.py
6
+ naturally_linux/executor.py
7
+ naturally_linux/generator.py
8
+ naturally_linux/safety.py
9
+ naturally_linux.egg-info/PKG-INFO
10
+ naturally_linux.egg-info/SOURCES.txt
11
+ naturally_linux.egg-info/dependency_links.txt
12
+ naturally_linux.egg-info/entry_points.txt
13
+ naturally_linux.egg-info/requires.txt
14
+ naturally_linux.egg-info/top_level.txt
15
+ tests/test_executor.py
16
+ tests/test_generator.py
17
+ tests/test_safety.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ naturally-linux = naturally_linux.cli:app
@@ -0,0 +1,4 @@
1
+ groq>=1.0.0
2
+ pytest>=9.0.2
3
+ python-dotenv>=1.2.1
4
+ typer>=0.21.1
@@ -0,0 +1 @@
1
+ naturally_linux
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "naturally-linux"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "groq>=1.0.0",
9
+ "pytest>=9.0.2",
10
+ "python-dotenv>=1.2.1",
11
+ "typer>=0.21.1",
12
+ ]
13
+
14
+ [build-system]
15
+ requires = ["setuptools>=68"]
16
+ build-backend = "setuptools.build_meta"
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["."]
20
+ include = ["naturally_linux*"]
21
+
22
+ [project.scripts]
23
+ naturally-linux = "naturally_linux.cli:app"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """Tests for command execution."""
2
+
3
+ from naturally_linux.executor import run_command
4
+
5
+
6
+ def test_run_command_echo():
7
+ stdout, stderr, returncode = run_command("echo hello")
8
+ assert returncode == 0
9
+ assert "hello" in stdout
10
+ assert stderr == ""
@@ -0,0 +1,33 @@
1
+ """Tests for command generator."""
2
+
3
+ from types import SimpleNamespace
4
+
5
+ import naturally_linux.generator as generator
6
+
7
+
8
+ class _FakeGroq:
9
+ """Minimal fake Groq client for tests."""
10
+
11
+ def __init__(self, api_key: str):
12
+ self.api_key = api_key
13
+
14
+ class chat:
15
+ class completions:
16
+ @staticmethod
17
+ def create(*_args, **_kwargs):
18
+ return SimpleNamespace(
19
+ choices=[
20
+ SimpleNamespace(
21
+ message=SimpleNamespace(content="ls -la")
22
+ )
23
+ ]
24
+ )
25
+
26
+
27
+ def test_generate_command_returns_string(monkeypatch):
28
+ monkeypatch.setenv("GROQ_API_KEY", "test-key")
29
+ monkeypatch.setattr(generator, "Groq", _FakeGroq)
30
+
31
+ result = generator.generate_command("list files")
32
+ assert isinstance(result, str)
33
+ assert result == "ls -la"
@@ -0,0 +1,11 @@
1
+ """Tests for safety checks."""
2
+
3
+ from naturally_linux.safety import is_safe_command
4
+
5
+
6
+ def test_is_safe_command_allows_benign():
7
+ assert is_safe_command("ls -la") is True
8
+
9
+
10
+ def test_is_safe_command_blocks_dangerous():
11
+ assert is_safe_command("rm -rf /") is False