aish-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aish/__init__.py +0 -0
- aish/__main__.py +3 -0
- aish/cli.py +160 -0
- aish/config.py +70 -0
- aish/executor.py +52 -0
- aish/history.py +67 -0
- aish/llm.py +88 -0
- aish/safety.py +70 -0
- aish_cli-0.1.0.dist-info/METADATA +109 -0
- aish_cli-0.1.0.dist-info/RECORD +14 -0
- aish_cli-0.1.0.dist-info/WHEEL +5 -0
- aish_cli-0.1.0.dist-info/entry_points.txt +2 -0
- aish_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- aish_cli-0.1.0.dist-info/top_level.txt +1 -0
aish/__init__.py
ADDED
|
File without changes
|
aish/__main__.py
ADDED
aish/cli.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from aish.config import AishConfig, ConfigNotFoundError, read_config, write_config
|
|
11
|
+
from aish.executor import run_command
|
|
12
|
+
from aish.history import append_history
|
|
13
|
+
from aish.llm import generate_command
|
|
14
|
+
from aish.safety import RiskLevel, check_command
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(name="aish", help="AI-powered shell assistant")
|
|
17
|
+
console = Console()
|
|
18
|
+
err_console = Console(stderr=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command()
|
|
22
|
+
def init(
|
|
23
|
+
base_url: Optional[str] = typer.Option(None, "--base-url", help="LLM API base URL"),
|
|
24
|
+
api_key: Optional[str] = typer.Option(None, "--api-key", help="LLM API key"),
|
|
25
|
+
model: Optional[str] = typer.Option(None, "--model", help="Model name"),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Configure aish with LLM API credentials."""
|
|
28
|
+
resolved_base_url: str = (
|
|
29
|
+
base_url if base_url is not None else typer.prompt("Base URL")
|
|
30
|
+
)
|
|
31
|
+
resolved_api_key: str = (
|
|
32
|
+
api_key if api_key is not None else typer.prompt("API key", hide_input=True)
|
|
33
|
+
)
|
|
34
|
+
resolved_model: str = (
|
|
35
|
+
model if model is not None else typer.prompt("Model", default="gpt-4o")
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
write_config(
|
|
40
|
+
AishConfig(
|
|
41
|
+
base_url=resolved_base_url,
|
|
42
|
+
api_key=resolved_api_key,
|
|
43
|
+
model=resolved_model,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
console.print(
|
|
47
|
+
"[bold green]✓[/bold green] Configuration saved to [cyan]~/.aish/config[/cyan]"
|
|
48
|
+
)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
err_console.print(f"[bold red]Error:[/bold red] Failed to save config: {e}")
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command()
|
|
55
|
+
def run(
|
|
56
|
+
prompt: list[str] = typer.Argument(..., help="Natural language request"),
|
|
57
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
58
|
+
dry_run: bool = typer.Option(
|
|
59
|
+
False, "--dry-run", "-d", help="Print command but do not execute"
|
|
60
|
+
),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Generate and execute a shell command from natural language."""
|
|
63
|
+
user_prompt = " ".join(prompt)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
config = read_config()
|
|
67
|
+
except ConfigNotFoundError:
|
|
68
|
+
err_console.print(
|
|
69
|
+
"[bold red]Error:[/bold red] No configuration found. Run [cyan]aish init[/cyan] first."
|
|
70
|
+
)
|
|
71
|
+
raise typer.Exit(1)
|
|
72
|
+
|
|
73
|
+
with console.status("[bold cyan]Generating command…[/bold cyan]"):
|
|
74
|
+
try:
|
|
75
|
+
output = generate_command(
|
|
76
|
+
user_prompt, config.base_url, config.api_key, config.model
|
|
77
|
+
)
|
|
78
|
+
except ValueError as e:
|
|
79
|
+
err_console.print(f"[bold red]Error:[/bold red] {e}")
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
|
|
82
|
+
risk_color = {"low": "green", "medium": "yellow", "high": "red"}.get(
|
|
83
|
+
output.risk_level, "white"
|
|
84
|
+
)
|
|
85
|
+
console.print(
|
|
86
|
+
Panel(
|
|
87
|
+
f"[bold]{output.command}[/bold]",
|
|
88
|
+
title="[bold]Command[/bold]",
|
|
89
|
+
border_style=risk_color,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
console.print(f"[dim]Explanation:[/dim] {output.explanation}")
|
|
93
|
+
console.print(f"[dim]Risk:[/dim] [{risk_color}]{output.risk_level}[/{risk_color}]")
|
|
94
|
+
|
|
95
|
+
safety_level, _ = check_command(output.command)
|
|
96
|
+
|
|
97
|
+
if safety_level == RiskLevel.DENY:
|
|
98
|
+
err_console.print(
|
|
99
|
+
"[bold red]✗ Denied:[/bold red] This command matches a dangerous pattern and cannot be executed."
|
|
100
|
+
)
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
confirmed = False
|
|
104
|
+
|
|
105
|
+
if safety_level == RiskLevel.WARN:
|
|
106
|
+
if output.risk_tip:
|
|
107
|
+
console.print(f"[bold yellow]⚠ Warning:[/bold yellow] {output.risk_tip}")
|
|
108
|
+
confirmed = typer.confirm(
|
|
109
|
+
"This command is potentially dangerous. Execute anyway?"
|
|
110
|
+
)
|
|
111
|
+
if not confirmed:
|
|
112
|
+
console.print("[yellow]Aborted.[/yellow]")
|
|
113
|
+
raise typer.Exit(0)
|
|
114
|
+
|
|
115
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
116
|
+
|
|
117
|
+
if dry_run:
|
|
118
|
+
console.print("[bold yellow]Dry run — not executing.[/bold yellow]")
|
|
119
|
+
append_history(
|
|
120
|
+
{
|
|
121
|
+
"timestamp": timestamp,
|
|
122
|
+
"prompt": user_prompt,
|
|
123
|
+
"command": output.command,
|
|
124
|
+
"exit_code": None,
|
|
125
|
+
"executed": False,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
raise typer.Exit(0)
|
|
129
|
+
|
|
130
|
+
if not yes and not confirmed:
|
|
131
|
+
confirmed = typer.confirm("Execute this command?")
|
|
132
|
+
if not confirmed:
|
|
133
|
+
console.print("[yellow]Aborted.[/yellow]")
|
|
134
|
+
raise typer.Exit(0)
|
|
135
|
+
|
|
136
|
+
result = run_command(output.command)
|
|
137
|
+
|
|
138
|
+
if result.stdout:
|
|
139
|
+
console.print(Panel(result.stdout.rstrip(), title="stdout", border_style="dim"))
|
|
140
|
+
if result.stderr:
|
|
141
|
+
console.print(
|
|
142
|
+
Panel(result.stderr.rstrip(), title="stderr", border_style="red dim")
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
exit_label = "[green]✓[/green]" if result.success else "[red]✗[/red]"
|
|
146
|
+
console.print(f"{exit_label} Exit code: [bold]{result.exit_code}[/bold]")
|
|
147
|
+
|
|
148
|
+
append_history(
|
|
149
|
+
{
|
|
150
|
+
"timestamp": timestamp,
|
|
151
|
+
"prompt": user_prompt,
|
|
152
|
+
"command": output.command,
|
|
153
|
+
"exit_code": result.exit_code,
|
|
154
|
+
"executed": True,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
app()
|
aish/config.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tomllib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import tomli_w
|
|
8
|
+
|
|
9
|
+
CONFIG_DIR = Path.home() / ".aish"
|
|
10
|
+
CONFIG_PATH = CONFIG_DIR / "config"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigNotFoundError(Exception):
|
|
14
|
+
"""Raised when ~/.aish/config does not exist."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfigInvalidError(Exception):
|
|
20
|
+
"""Raised when config exists but is missing required fields."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class AishConfig:
|
|
27
|
+
base_url: str
|
|
28
|
+
api_key: str
|
|
29
|
+
model: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_config() -> AishConfig:
|
|
33
|
+
"""Read config from ~/.aish/config. Raises ConfigNotFoundError if not found."""
|
|
34
|
+
if not CONFIG_PATH.exists():
|
|
35
|
+
raise ConfigNotFoundError("Run 'aish init' first")
|
|
36
|
+
with open(CONFIG_PATH, "rb") as f:
|
|
37
|
+
data = tomllib.load(f)
|
|
38
|
+
missing = [k for k in ("base_url", "api_key", "model") if k not in data]
|
|
39
|
+
if missing:
|
|
40
|
+
raise ConfigInvalidError(
|
|
41
|
+
f"Config missing required fields: {', '.join(missing)}"
|
|
42
|
+
)
|
|
43
|
+
return AishConfig(
|
|
44
|
+
base_url=data["base_url"],
|
|
45
|
+
api_key=data["api_key"],
|
|
46
|
+
model=data["model"],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def write_config(config: AishConfig) -> None:
|
|
51
|
+
"""Write config to ~/.aish/config as TOML."""
|
|
52
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
data = {
|
|
54
|
+
"base_url": config.base_url,
|
|
55
|
+
"api_key": config.api_key,
|
|
56
|
+
"model": config.model,
|
|
57
|
+
}
|
|
58
|
+
with open(CONFIG_PATH, "wb") as f:
|
|
59
|
+
tomli_w.dump(data, f)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def config_exists() -> bool:
|
|
63
|
+
"""Return True if config file exists and has required fields."""
|
|
64
|
+
if not CONFIG_PATH.exists():
|
|
65
|
+
return False
|
|
66
|
+
try:
|
|
67
|
+
read_config()
|
|
68
|
+
return True
|
|
69
|
+
except Exception:
|
|
70
|
+
return False
|
aish/executor.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Command execution wrapper for aish."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ExecResult:
|
|
10
|
+
command: str
|
|
11
|
+
exit_code: int
|
|
12
|
+
stdout: str
|
|
13
|
+
stderr: str
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def success(self) -> bool:
|
|
17
|
+
return self.exit_code == 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_command(command: str, timeout: int = 30) -> ExecResult:
|
|
21
|
+
"""
|
|
22
|
+
Execute a shell command using shell=True (supports pipes, redirects).
|
|
23
|
+
Returns ExecResult with exit code, stdout, stderr.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
result = subprocess.run(
|
|
27
|
+
command,
|
|
28
|
+
shell=True,
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
timeout=timeout,
|
|
32
|
+
)
|
|
33
|
+
return ExecResult(
|
|
34
|
+
command=command,
|
|
35
|
+
exit_code=result.returncode,
|
|
36
|
+
stdout=result.stdout,
|
|
37
|
+
stderr=result.stderr,
|
|
38
|
+
)
|
|
39
|
+
except subprocess.TimeoutExpired:
|
|
40
|
+
return ExecResult(
|
|
41
|
+
command=command,
|
|
42
|
+
exit_code=124, # Standard timeout exit code
|
|
43
|
+
stdout="",
|
|
44
|
+
stderr=f"Command timed out after {timeout} seconds",
|
|
45
|
+
)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return ExecResult(
|
|
48
|
+
command=command,
|
|
49
|
+
exit_code=1,
|
|
50
|
+
stdout="",
|
|
51
|
+
stderr=str(e),
|
|
52
|
+
)
|
aish/history.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Command history management for aish — JSON Lines format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
HISTORY_DIR = Path.home() / ".aish"
|
|
10
|
+
HISTORY_PATH = HISTORY_DIR / "history"
|
|
11
|
+
MAX_ENTRIES = 1000
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def append_history(entry: dict) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Append one history entry to ~/.aish/history (JSON Lines format).
|
|
17
|
+
Auto-trims to MAX_ENTRIES by removing oldest entries.
|
|
18
|
+
|
|
19
|
+
Expected entry fields:
|
|
20
|
+
timestamp: str (ISO 8601) — auto-added if missing
|
|
21
|
+
prompt: str
|
|
22
|
+
command: str
|
|
23
|
+
exit_code: int | None
|
|
24
|
+
executed: bool
|
|
25
|
+
"""
|
|
26
|
+
if "timestamp" not in entry:
|
|
27
|
+
entry = {**entry, "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
28
|
+
|
|
29
|
+
HISTORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
existing: list[dict] = []
|
|
32
|
+
if HISTORY_PATH.exists():
|
|
33
|
+
with open(HISTORY_PATH, "r", encoding="utf-8") as f:
|
|
34
|
+
for line in f:
|
|
35
|
+
line = line.strip()
|
|
36
|
+
if line:
|
|
37
|
+
try:
|
|
38
|
+
existing.append(json.loads(line))
|
|
39
|
+
except json.JSONDecodeError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
existing.append(entry)
|
|
43
|
+
|
|
44
|
+
if len(existing) > MAX_ENTRIES:
|
|
45
|
+
existing = existing[-MAX_ENTRIES:]
|
|
46
|
+
|
|
47
|
+
with open(HISTORY_PATH, "w", encoding="utf-8") as f:
|
|
48
|
+
for e in existing:
|
|
49
|
+
f.write(json.dumps(e, ensure_ascii=False) + "\n")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def read_history() -> list[dict]:
|
|
53
|
+
"""Return all history entries, newest first."""
|
|
54
|
+
if not HISTORY_PATH.exists():
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
entries: list[dict] = []
|
|
58
|
+
with open(HISTORY_PATH, "r", encoding="utf-8") as f:
|
|
59
|
+
for line in f:
|
|
60
|
+
line = line.strip()
|
|
61
|
+
if line:
|
|
62
|
+
try:
|
|
63
|
+
entries.append(json.loads(line))
|
|
64
|
+
except json.JSONDecodeError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return list(reversed(entries))
|
aish/llm.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""LLM integration for aish — OpenAI-compatible API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import json
|
|
5
|
+
import platform
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from openai import OpenAI
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CommandOutput:
|
|
13
|
+
command: str
|
|
14
|
+
explanation: str
|
|
15
|
+
risk_level: str # "low", "medium", "high"
|
|
16
|
+
risk_tip: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
SYSTEM_PROMPT = """You are a shell command generator. Your job is to convert natural language requests into executable shell commands.
|
|
20
|
+
|
|
21
|
+
Rules:
|
|
22
|
+
1. Return ONLY valid JSON — no markdown, no code blocks, no extra text
|
|
23
|
+
2. Return EXACTLY ONE command
|
|
24
|
+
3. The command must be executable on {os_name} ({shell})
|
|
25
|
+
4. JSON schema: {{"command": "...", "explanation": "...", "risk_level": "low|medium|high", "risk_tip": "..."}}
|
|
26
|
+
5. risk_level: "low" for safe commands, "medium" for potentially impactful, "high" for destructive/irreversible
|
|
27
|
+
6. risk_tip: brief warning if risk_level is medium/high, otherwise empty string
|
|
28
|
+
7. If the request is ambiguous or dangerous, still return the most reasonable command but set risk_level to "high"
|
|
29
|
+
|
|
30
|
+
Example response:
|
|
31
|
+
{{"command": "ls -la", "explanation": "Lists all files in current directory with details", "risk_level": "low", "risk_tip": ""}}"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def generate_command(
|
|
35
|
+
prompt: str,
|
|
36
|
+
base_url: str,
|
|
37
|
+
api_key: str,
|
|
38
|
+
model: str,
|
|
39
|
+
) -> CommandOutput:
|
|
40
|
+
"""
|
|
41
|
+
Call the LLM API to generate a shell command from a natural language prompt.
|
|
42
|
+
Raises ValueError if the response cannot be parsed.
|
|
43
|
+
"""
|
|
44
|
+
client = OpenAI(api_key=api_key, base_url=base_url)
|
|
45
|
+
|
|
46
|
+
os_name = platform.system()
|
|
47
|
+
shell = "bash" # default shell assumption
|
|
48
|
+
|
|
49
|
+
system_msg = SYSTEM_PROMPT.format(os_name=os_name, shell=shell)
|
|
50
|
+
|
|
51
|
+
response = client.chat.completions.create(
|
|
52
|
+
model=model,
|
|
53
|
+
messages=[
|
|
54
|
+
{"role": "system", "content": system_msg},
|
|
55
|
+
{"role": "user", "content": prompt},
|
|
56
|
+
],
|
|
57
|
+
temperature=0,
|
|
58
|
+
timeout=60,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
content = response.choices[0].message.content
|
|
62
|
+
if not content:
|
|
63
|
+
raise ValueError("LLM returned empty response")
|
|
64
|
+
|
|
65
|
+
# Strip markdown code blocks if present
|
|
66
|
+
content = content.strip()
|
|
67
|
+
if content.startswith("```"):
|
|
68
|
+
lines = content.split("\n")
|
|
69
|
+
# Remove first and last lines (``` markers)
|
|
70
|
+
content = "\n".join(lines[1:-1] if lines[-1] == "```" else lines[1:])
|
|
71
|
+
content = content.strip()
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
data = json.loads(content)
|
|
75
|
+
except json.JSONDecodeError as e:
|
|
76
|
+
raise ValueError(f"LLM returned invalid JSON: {e}\nContent: {content}") from e
|
|
77
|
+
|
|
78
|
+
required_fields = ("command", "explanation", "risk_level", "risk_tip")
|
|
79
|
+
missing = [f for f in required_fields if f not in data]
|
|
80
|
+
if missing:
|
|
81
|
+
raise ValueError(f"LLM response missing fields: {missing}")
|
|
82
|
+
|
|
83
|
+
return CommandOutput(
|
|
84
|
+
command=data["command"],
|
|
85
|
+
explanation=data["explanation"],
|
|
86
|
+
risk_level=data.get("risk_level", "low"),
|
|
87
|
+
risk_tip=data.get("risk_tip", ""),
|
|
88
|
+
)
|
aish/safety.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Dangerous command detection for aish."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import re
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RiskLevel(Enum):
|
|
9
|
+
ALLOW = "allow"
|
|
10
|
+
WARN = "warn"
|
|
11
|
+
DENY = "deny"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Patterns that DENY execution outright
|
|
15
|
+
DENY_PATTERNS = [
|
|
16
|
+
r":\s*\(\s*\)\s*\{", # fork bomb: :(){
|
|
17
|
+
r"\bmkfs\b", # format filesystem
|
|
18
|
+
r"\bfdisk\b.*--wipe", # disk wipe
|
|
19
|
+
r"dd\s+if=.*of=/dev/(sd|hd|nvme|disk)", # dd to raw disk
|
|
20
|
+
r">\s*/dev/(sd[a-z]|hd[a-z]|nvme\d)", # redirect to raw disk
|
|
21
|
+
r"shred\s+.*(/dev/|/boot/)", # shred critical paths
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Patterns that WARN and require explicit confirmation
|
|
25
|
+
WARN_PATTERNS = [
|
|
26
|
+
r"\brm\s+(-[rfRF]+\s+|-[rfRF]+$)", # rm with -r or -f flags
|
|
27
|
+
r"\brm\s+-[a-zA-Z]*[rf][a-zA-Z]*", # rm with r or f anywhere in flags
|
|
28
|
+
r"\bsudo\b", # sudo usage
|
|
29
|
+
r"chmod\s+(777|[0-7]{3})\s+", # chmod
|
|
30
|
+
r"\|\s*(sh|bash|zsh|fish)\b", # pipe to shell
|
|
31
|
+
r"\bwget\b.*\|\s*(sh|bash)", # wget | bash
|
|
32
|
+
r"\bcurl\b.*\|\s*(sh|bash)", # curl | bash
|
|
33
|
+
r"\bdd\b.*\bof=", # dd with output file
|
|
34
|
+
r">\s*/etc/", # redirect to /etc/
|
|
35
|
+
r">\s*/usr/", # redirect to /usr/
|
|
36
|
+
r">\s*/boot/", # redirect to /boot/
|
|
37
|
+
r"\bchown\b.*-[Rr]", # recursive chown
|
|
38
|
+
r"\bkillall\b", # killall
|
|
39
|
+
r"\bpkill\b", # pkill
|
|
40
|
+
r"\bsystemctl\b.*(stop|disable|mask)", # stop system services
|
|
41
|
+
r"\buseradd\b|\buserdel\b|\busermod\b", # user management
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_command(command: str) -> tuple[RiskLevel, list[str]]:
|
|
46
|
+
"""
|
|
47
|
+
Check a command for dangerous patterns.
|
|
48
|
+
Returns (RiskLevel, list_of_matched_patterns).
|
|
49
|
+
"""
|
|
50
|
+
matches: list[str] = []
|
|
51
|
+
|
|
52
|
+
for pattern in DENY_PATTERNS:
|
|
53
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
54
|
+
matches.append(pattern)
|
|
55
|
+
return RiskLevel.DENY, matches
|
|
56
|
+
|
|
57
|
+
for pattern in WARN_PATTERNS:
|
|
58
|
+
if re.search(pattern, command, re.IGNORECASE):
|
|
59
|
+
matches.append(pattern)
|
|
60
|
+
|
|
61
|
+
if matches:
|
|
62
|
+
return RiskLevel.WARN, matches
|
|
63
|
+
|
|
64
|
+
return RiskLevel.ALLOW, []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_dangerous(command: str) -> bool:
|
|
68
|
+
"""Returns True if the command is WARN or DENY level."""
|
|
69
|
+
level, _ = check_command(command)
|
|
70
|
+
return level != RiskLevel.ALLOW
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aish-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-powered shell command assistant - convert natural language to safe executable commands
|
|
5
|
+
Author-email: Your Name <your.email@example.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Kuroome
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/yourusername/aish
|
|
29
|
+
Project-URL: Repository, https://github.com/yourusername/aish.git
|
|
30
|
+
Project-URL: Issues, https://github.com/yourusername/aish/issues
|
|
31
|
+
Keywords: ai,shell,cli,command,assistant,gpt
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: Intended Audience :: System Administrators
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: System :: Shells
|
|
44
|
+
Classifier: Topic :: Utilities
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
License-File: LICENSE
|
|
48
|
+
Requires-Dist: openai>=1.0.0
|
|
49
|
+
Requires-Dist: typer[all]>=0.9.0
|
|
50
|
+
Requires-Dist: rich>=13.0.0
|
|
51
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
52
|
+
Dynamic: license-file
|
|
53
|
+
|
|
54
|
+
# aish (AI Shell)
|
|
55
|
+
|
|
56
|
+
Convert natural language into safe shell commands. `aish` uses an LLM to translate your intent into bash, validates it for safety, and executes it.
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
Python 3.10 or higher.
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone <repo>
|
|
66
|
+
cd aish
|
|
67
|
+
pip install -e .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
Set up your LLM provider by running `aish init`.
|
|
73
|
+
|
|
74
|
+
**Interactive mode:**
|
|
75
|
+
```bash
|
|
76
|
+
aish init
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Flag mode:**
|
|
80
|
+
```bash
|
|
81
|
+
aish init --base-url "https://api.openai.com/v1" --api-key "sk-..." --model "gpt-4o"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
Pass your prompt directly to `aish run`. No quotes needed.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
aish run list all python files
|
|
90
|
+
aish run show disk usage --dry-run
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Options
|
|
94
|
+
|
|
95
|
+
| Option | Short | Description |
|
|
96
|
+
|-------------|-------|------------------------------------|
|
|
97
|
+
| `--yes` | `-y` | Skip confirmation for safe commands |
|
|
98
|
+
| `--dry-run` | `-d` | Show the command without executing |
|
|
99
|
+
|
|
100
|
+
## Safety
|
|
101
|
+
|
|
102
|
+
Built-in validation checks every command before execution:
|
|
103
|
+
* **ALLOW**: Low risk. Runs immediately if you use `--yes`.
|
|
104
|
+
* **WARN**: Medium risk. Always asks for confirmation, even with `--yes`.
|
|
105
|
+
* **DENY**: High risk (like disk wipes or fork bombs). Blocked completely.
|
|
106
|
+
|
|
107
|
+
## History
|
|
108
|
+
|
|
109
|
+
Your last 1000 commands are saved to `~/.aish/history` in JSON Lines format. Old commands are automatically trimmed to save space.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
aish/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
aish/__main__.py,sha256=H8LVHcbmXX3b2jiXLrNcRiVqu6GXnfk7Be--8ZMkRkA,32
|
|
3
|
+
aish/cli.py,sha256=t-aySkc7-xF5MumI4LYGt60noasW2CAp2Z6YO1AUD8g,5185
|
|
4
|
+
aish/config.py,sha256=SKDYPdjPHSK8Dh35xZZIuJRhroaLX87pGQJmoZXMYM8,1698
|
|
5
|
+
aish/executor.py,sha256=ipYLUnYbUYpk-sxBM3mZ6zCbnhZPY8FTvxVFadMK-aM,1295
|
|
6
|
+
aish/history.py,sha256=j_if9FJJeCEJYLVbdQtvfUClfa0ZrGl_fHWNguqVkF8,1915
|
|
7
|
+
aish/llm.py,sha256=-RmFPOit3F3f86isIekF5xWZHwzo-gt36fmYTBz_6IY,2947
|
|
8
|
+
aish/safety.py,sha256=xH16kel4mqAbwi6NPmKs_CpRaEGzlxZm3bnDr2j-zE0,2159
|
|
9
|
+
aish_cli-0.1.0.dist-info/licenses/LICENSE,sha256=cJrOgdtzXdci9K21Se1167ziZ372z3f3hWqLiBxkfgI,1064
|
|
10
|
+
aish_cli-0.1.0.dist-info/METADATA,sha256=H0fWrG0mDhPKd2o2Z6ylVrQjsyUW96CuxC0SqhiiQfc,3847
|
|
11
|
+
aish_cli-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
12
|
+
aish_cli-0.1.0.dist-info/entry_points.txt,sha256=WHIWEAjYaVujCn3WhS4KC_Aa-N3awfv-A3fRHPWN_IU,38
|
|
13
|
+
aish_cli-0.1.0.dist-info/top_level.txt,sha256=7IoxNdXDpw3hvEZcAbaz7vhZj_WPTcM-zFXTDXVg47M,5
|
|
14
|
+
aish_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kuroome
|
|
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
|
+
aish
|