itamar-agentcli 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.
Files changed (26) hide show
  1. itamar_agentcli-0.1.0/PKG-INFO +9 -0
  2. itamar_agentcli-0.1.0/itamar_agentcli.egg-info/PKG-INFO +9 -0
  3. itamar_agentcli-0.1.0/itamar_agentcli.egg-info/SOURCES.txt +24 -0
  4. itamar_agentcli-0.1.0/itamar_agentcli.egg-info/dependency_links.txt +1 -0
  5. itamar_agentcli-0.1.0/itamar_agentcli.egg-info/entry_points.txt +2 -0
  6. itamar_agentcli-0.1.0/itamar_agentcli.egg-info/requires.txt +4 -0
  7. itamar_agentcli-0.1.0/itamar_agentcli.egg-info/top_level.txt +1 -0
  8. itamar_agentcli-0.1.0/localagent/__init__.py +0 -0
  9. itamar_agentcli-0.1.0/localagent/agent/__init__.py +0 -0
  10. itamar_agentcli-0.1.0/localagent/agent/planner.py +0 -0
  11. itamar_agentcli-0.1.0/localagent/agent_parser.py +22 -0
  12. itamar_agentcli-0.1.0/localagent/cli.py +36 -0
  13. itamar_agentcli-0.1.0/localagent/core/__init__.py +0 -0
  14. itamar_agentcli-0.1.0/localagent/core/context.py +0 -0
  15. itamar_agentcli-0.1.0/localagent/core/errors.py +0 -0
  16. itamar_agentcli-0.1.0/localagent/core/models.py +0 -0
  17. itamar_agentcli-0.1.0/localagent/gateway/__init__.py +0 -0
  18. itamar_agentcli-0.1.0/localagent/gateway/openai_compat.py +70 -0
  19. itamar_agentcli-0.1.0/localagent/safety/__init__.py +0 -0
  20. itamar_agentcli-0.1.0/localagent/safety/approval.py +75 -0
  21. itamar_agentcli-0.1.0/localagent/tools/__init__.py +0 -0
  22. itamar_agentcli-0.1.0/localagent/tools/exec.py +152 -0
  23. itamar_agentcli-0.1.0/localagent/tools/files.py +0 -0
  24. itamar_agentcli-0.1.0/localagent/tools/python_runner.py +0 -0
  25. itamar_agentcli-0.1.0/pyproject.toml +18 -0
  26. itamar_agentcli-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: itamar-agentcli
3
+ Version: 0.1.0
4
+ Summary: Local AI Agent CLI
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: typer>=0.12
7
+ Requires-Dist: rich>=13
8
+ Requires-Dist: pydantic>=2
9
+ Requires-Dist: requests>=2
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: itamar-agentcli
3
+ Version: 0.1.0
4
+ Summary: Local AI Agent CLI
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: typer>=0.12
7
+ Requires-Dist: rich>=13
8
+ Requires-Dist: pydantic>=2
9
+ Requires-Dist: requests>=2
@@ -0,0 +1,24 @@
1
+ pyproject.toml
2
+ itamar_agentcli.egg-info/PKG-INFO
3
+ itamar_agentcli.egg-info/SOURCES.txt
4
+ itamar_agentcli.egg-info/dependency_links.txt
5
+ itamar_agentcli.egg-info/entry_points.txt
6
+ itamar_agentcli.egg-info/requires.txt
7
+ itamar_agentcli.egg-info/top_level.txt
8
+ localagent/__init__.py
9
+ localagent/agent_parser.py
10
+ localagent/cli.py
11
+ localagent/agent/__init__.py
12
+ localagent/agent/planner.py
13
+ localagent/core/__init__.py
14
+ localagent/core/context.py
15
+ localagent/core/errors.py
16
+ localagent/core/models.py
17
+ localagent/gateway/__init__.py
18
+ localagent/gateway/openai_compat.py
19
+ localagent/safety/__init__.py
20
+ localagent/safety/approval.py
21
+ localagent/tools/__init__.py
22
+ localagent/tools/exec.py
23
+ localagent/tools/files.py
24
+ localagent/tools/python_runner.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agentcli = localagent.cli:app
@@ -0,0 +1,4 @@
1
+ typer>=0.12
2
+ rich>=13
3
+ pydantic>=2
4
+ requests>=2
File without changes
File without changes
File without changes
@@ -0,0 +1,22 @@
1
+ import re
2
+
3
+ def parse_command(text: str) -> str | None:
4
+ text = text.lower()
5
+
6
+ # install packages
7
+ if "nmap" in text:
8
+ return "sudo dnf install -y nmap"
9
+
10
+ if "ffmpeg" in text:
11
+ return "sudo dnf install -y ffmpeg --allowerasing"
12
+
13
+ if "yt-dlp" in text or "youtube" in text:
14
+ return "sudo dnf install -y yt-dlp"
15
+
16
+ if "update" in text or "עדכן" in text:
17
+ return "sudo dnf update -y"
18
+
19
+ if "נקה" in text or "clean" in text:
20
+ return "sudo dnf autoremove -y"
21
+
22
+ return None
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+ from rich.console import Console
5
+
6
+ from localagent.tools.exec import CommandExecutor
7
+
8
+ console = Console()
9
+ app = typer.Typer(add_completion=False)
10
+
11
+
12
+ @app.command()
13
+ def main(
14
+ command: str = typer.Argument(
15
+ ...,
16
+ help='Command to run. Quote it if it contains spaces, e.g. "ls -la"',
17
+ ),
18
+ yes: bool = typer.Option(
19
+ False,
20
+ "--yes",
21
+ help="Auto-approve ONLY whitelisted dangerous commands (dnf install/update/upgrade/remove).",
22
+ ),
23
+ stream: bool = typer.Option(
24
+ False,
25
+ "--stream",
26
+ help="Stream stdout live while running.",
27
+ ),
28
+ ):
29
+ """LocalAgent command runner (with approvals)."""
30
+ executor = CommandExecutor(console=console, auto_yes=yes)
31
+ result = executor.run(command=command, require_approval=True, stream=stream)
32
+ raise typer.Exit(code=result.exit_code)
33
+
34
+
35
+ if __name__ == "__main__":
36
+ app()
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+ import requests
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ChatMessage(BaseModel):
9
+ role: str # "system" | "user" | "assistant" | "tool"
10
+ content: str
11
+
12
+
13
+ class GatewayResponse(BaseModel):
14
+ content: str
15
+ raw: Dict[str, Any] = Field(default_factory=dict)
16
+
17
+
18
+ class OpenAICompatGateway:
19
+ """
20
+ Minimal OpenAI-compatible Chat Completions gateway.
21
+ Intended for local LLM servers (Ollama OpenAI compatibility, vLLM, etc.).
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ base_url: str = "http://localhost:11434/v1",
27
+ api_key: str = "ollama", # often ignored by local servers
28
+ model: str = "llama3.1",
29
+ timeout_s: int = 120,
30
+ ) -> None:
31
+ self.base_url = base_url.rstrip("/")
32
+ self.api_key = api_key
33
+ self.model = model
34
+ self.timeout_s = timeout_s
35
+
36
+ def chat(
37
+ self,
38
+ messages: List[ChatMessage],
39
+ temperature: float = 0.2,
40
+ max_tokens: Optional[int] = None,
41
+ extra: Optional[Dict[str, Any]] = None,
42
+ ) -> GatewayResponse:
43
+ url = f"{self.base_url}/chat/completions"
44
+ payload: Dict[str, Any] = {
45
+ "model": self.model,
46
+ "messages": [m.model_dump() for m in messages],
47
+ "temperature": temperature,
48
+ }
49
+ if max_tokens is not None:
50
+ payload["max_tokens"] = max_tokens
51
+ if extra:
52
+ payload.update(extra)
53
+
54
+ headers = {
55
+ "Authorization": f"Bearer {self.api_key}",
56
+ "Content-Type": "application/json",
57
+ }
58
+
59
+ r = requests.post(url, json=payload, headers=headers, timeout=self.timeout_s)
60
+ r.raise_for_status()
61
+ data = r.json()
62
+
63
+ # Standard OpenAI format: choices[0].message.content
64
+ content = (
65
+ data.get("choices", [{}])[0]
66
+ .get("message", {})
67
+ .get("content", "")
68
+ )
69
+
70
+ return GatewayResponse(content=content, raw=data)
File without changes
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import Optional, Sequence
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class ApprovalDecision:
10
+ approved: bool
11
+ reason: str
12
+
13
+
14
+ class ApprovalPolicy:
15
+ """
16
+ Heuristic-based approval for shell commands + a small whitelist mechanism
17
+ for auto-approval (e.g., dnf installs) when --yes is used.
18
+ """
19
+
20
+ DEFAULT_DANGEROUS_PATTERNS: Sequence[str] = (
21
+ r"\brm\b.*\s-rf\b",
22
+ r"\brm\b.*\s--no-preserve-root\b",
23
+ r"\bdd\b",
24
+ r"\bmkfs\.",
25
+ r"\bchmod\b\s+777\b",
26
+ r"\bchown\b.*\s-R\b",
27
+ r"\bshutdown\b|\breboot\b",
28
+ r"\bsystemctl\b\s+(stop|disable|mask)\b",
29
+ r"\bdnf\b\s+(install|remove|upgrade|update)\b",
30
+ r"\byum\b\s+(install|remove|update|upgrade)\b",
31
+ r"\bpip\b\s+install\b",
32
+ r"\bcurl\b.*\|\s*(sh|bash)\b",
33
+ r"\bwget\b.*\|\s*(sh|bash)\b",
34
+ r"\bchmod\b.*\+x\b.*\.\w+",
35
+ r"\bsudo\b",
36
+ )
37
+
38
+ def __init__(self, extra_patterns: Optional[Sequence[str]] = None) -> None:
39
+ patterns = list(self.DEFAULT_DANGEROUS_PATTERNS)
40
+ if extra_patterns:
41
+ patterns.extend(extra_patterns)
42
+ self._compiled = [re.compile(p, re.IGNORECASE) for p in patterns]
43
+
44
+ def requires_approval(self, command: str) -> Optional[str]:
45
+ cmd = command.strip()
46
+ for rx in self._compiled:
47
+ if rx.search(cmd):
48
+ return f"Matched risky pattern: {rx.pattern}"
49
+ return None
50
+
51
+ def is_autoapprove_whitelisted(self, command: str) -> bool:
52
+ """
53
+ Commands allowed for auto-approval when --yes is used.
54
+ Keep this list tight and explicit.
55
+ """
56
+ cmd = command.strip().lower()
57
+
58
+ # Allow these admin package operations to auto-approve
59
+ whitelist_prefixes = (
60
+ "sudo dnf install",
61
+ "sudo dnf update",
62
+ "sudo dnf upgrade",
63
+ "sudo dnf remove",
64
+ "dnf install",
65
+ "dnf update",
66
+ "dnf upgrade",
67
+ "dnf remove",
68
+ "sudo dnf autoremove",
69
+ "dnf autoremove",
70
+ "sudo dnf clean",
71
+ "dnf clean",
72
+ "sudo dnf makecache",
73
+ "dnf makecache",
74
+ )
75
+ return cmd.startswith(whitelist_prefixes)
File without changes
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shlex
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from typing import Dict, List, Optional
8
+
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.prompt import Confirm
12
+
13
+ from localagent.safety.approval import ApprovalPolicy
14
+
15
+
16
+ @dataclass
17
+ class CommandResult:
18
+ command: str
19
+ cwd: str
20
+ exit_code: int
21
+ stdout: str
22
+ stderr: str
23
+
24
+
25
+ class CommandExecutor:
26
+ """
27
+ Executes shell commands with human-in-the-loop approval for risky commands.
28
+ If auto_yes=True, will auto-approve ONLY whitelisted risky commands.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ approval: Optional[ApprovalPolicy] = None,
34
+ console: Optional[Console] = None,
35
+ default_cwd: Optional[str] = None,
36
+ env_overrides: Optional[Dict[str, str]] = None,
37
+ auto_yes: bool = False,
38
+ ) -> None:
39
+ self.approval = approval or ApprovalPolicy()
40
+ self.console = console or Console()
41
+ self.default_cwd = default_cwd or os.getcwd()
42
+ self.env_overrides = env_overrides or {}
43
+ self.auto_yes = auto_yes
44
+
45
+ def run(
46
+ self,
47
+ command: str,
48
+ cwd: Optional[str] = None,
49
+ timeout_s: int = 600,
50
+ check: bool = False,
51
+ require_approval: bool = True,
52
+ stream: bool = False,
53
+ ) -> CommandResult:
54
+ cwd_final = cwd or self.default_cwd
55
+
56
+ # Approval step
57
+ if require_approval:
58
+ reason = self.approval.requires_approval(command)
59
+ if reason:
60
+ self.console.print(
61
+ Panel.fit(
62
+ f"[bold yellow]Approval required[/bold yellow]\n"
63
+ f"[bold]Reason:[/bold] {reason}\n\n"
64
+ f"[bold]Command:[/bold]\n{command}\n\n"
65
+ f"[dim]CWD:[/dim] {cwd_final}",
66
+ title="Dangerous Command Detected",
67
+ )
68
+ )
69
+
70
+ # Auto-approve only whitelisted commands when --yes is enabled
71
+ if self.auto_yes and self.approval.is_autoapprove_whitelisted(command):
72
+ self.console.print(
73
+ "[bold yellow]Auto-approval enabled (--yes + whitelist). Proceeding...[/bold yellow]"
74
+ )
75
+ else:
76
+ if not Confirm.ask("Execute this command?", default=False):
77
+ return CommandResult(
78
+ command=command,
79
+ cwd=cwd_final,
80
+ exit_code=130,
81
+ stdout="",
82
+ stderr="User rejected command execution.",
83
+ )
84
+
85
+ env = os.environ.copy()
86
+ env.update(self.env_overrides)
87
+
88
+ self.console.print(
89
+ Panel.fit(
90
+ f"[bold cyan]$[/bold cyan] {command}\n[dim]cwd:[/dim] {cwd_final}",
91
+ title="Executing",
92
+ )
93
+ )
94
+
95
+ args: List[str] = shlex.split(command)
96
+
97
+ if stream:
98
+ proc = subprocess.Popen(
99
+ args,
100
+ cwd=cwd_final,
101
+ env=env,
102
+ stdout=subprocess.PIPE,
103
+ stderr=subprocess.PIPE,
104
+ text=True,
105
+ bufsize=1,
106
+ )
107
+ out_lines: List[str] = []
108
+ err_lines: List[str] = []
109
+
110
+ assert proc.stdout is not None
111
+ assert proc.stderr is not None
112
+
113
+ for line in proc.stdout:
114
+ out_lines.append(line)
115
+ self.console.print(line.rstrip("\n"))
116
+
117
+ err_text = proc.stderr.read()
118
+ if err_text:
119
+ err_lines.append(err_text)
120
+ self.console.print(err_text, style="bold red")
121
+
122
+ exit_code = proc.wait(timeout=timeout_s)
123
+ stdout = "".join(out_lines)
124
+ stderr = "".join(err_lines)
125
+ else:
126
+ completed = subprocess.run(
127
+ args,
128
+ cwd=cwd_final,
129
+ env=env,
130
+ capture_output=True,
131
+ text=True,
132
+ timeout=timeout_s,
133
+ )
134
+ exit_code = completed.returncode
135
+ stdout = completed.stdout or ""
136
+ stderr = completed.stderr or ""
137
+
138
+ if stdout.strip():
139
+ self.console.print(Panel.fit(stdout.rstrip(), title="stdout"))
140
+ if stderr.strip():
141
+ self.console.print(Panel.fit(stderr.rstrip(), title="stderr", style="red"))
142
+
143
+ if check and exit_code != 0:
144
+ raise subprocess.CalledProcessError(exit_code, args, stdout, stderr)
145
+
146
+ return CommandResult(
147
+ command=command,
148
+ cwd=cwd_final,
149
+ exit_code=exit_code,
150
+ stdout=stdout,
151
+ stderr=stderr,
152
+ )
File without changes
@@ -0,0 +1,18 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "itamar-agentcli"
7
+ version = "0.1.0"
8
+ description = "Local AI Agent CLI"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "typer>=0.12",
12
+ "rich>=13",
13
+ "pydantic>=2",
14
+ "requests>=2",
15
+ ]
16
+
17
+ [project.scripts]
18
+ agentcli = "localagent.cli:app"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+