hey-cli-python 1.0.6__tar.gz → 1.0.8__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.
- {hey_cli_python-1.0.6/hey_cli_python.egg-info → hey_cli_python-1.0.8}/PKG-INFO +22 -1
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/README.md +21 -0
- hey_cli_python-1.0.8/hey_cli/cli.py +157 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli/llm.py +79 -39
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8/hey_cli_python.egg-info}/PKG-INFO +22 -1
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/pyproject.toml +1 -1
- hey_cli_python-1.0.6/hey_cli/cli.py +0 -109
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/LICENSE +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli/__init__.py +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli/governance.py +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli/history.py +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli/models.py +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli/runner.py +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli/skills.py +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli_python.egg-info/SOURCES.txt +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli_python.egg-info/dependency_links.txt +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli_python.egg-info/entry_points.txt +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli_python.egg-info/requires.txt +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/hey_cli_python.egg-info/top_level.txt +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/setup.cfg +0 -0
- {hey_cli_python-1.0.6 → hey_cli_python-1.0.8}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hey-cli-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.8
|
|
4
4
|
Summary: A secure, zero-bloat CLI companion that turns natural language and error logs into executable commands.
|
|
5
5
|
Author: Mohit Singh Sinsniwal
|
|
6
6
|
Project-URL: Homepage, https://github.com/sinsniwal/hey-cli
|
|
@@ -73,6 +73,10 @@ brew tap sinsniwal/hey-cli
|
|
|
73
73
|
brew install hey-cli
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
> [!TIP]
|
|
77
|
+
> **Apple Silicon (M1/M2/M3) Note:** If you see an error about `Rosetta 2` while installing via Homebrew, ensure your terminal is running natively (not under Rosetta emulation). You can force a native installation by running:
|
|
78
|
+
> `arch -arm64 brew install hey-cli`
|
|
79
|
+
|
|
76
80
|
### macOS & Linux (curl)
|
|
77
81
|
|
|
78
82
|
```bash
|
|
@@ -133,6 +137,23 @@ hey <your objective in plain English>
|
|
|
133
137
|
|
|
134
138
|
---
|
|
135
139
|
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Authentication & Custom Endpoints
|
|
144
|
+
|
|
145
|
+
`hey` works locally by default, but it also supports authenticated Ollama instances and custom hosts:
|
|
146
|
+
|
|
147
|
+
- **Standard Login:** Most users should run `ollama login` once to authenticate their terminal with the local or cloud instance.
|
|
148
|
+
- **Auth Key:** If you are in a CI/CD or server environment, you can set the `OLLAMA_API_KEY` environment variable.
|
|
149
|
+
- **Custom Host:** If Ollama is running on a different port or machine, set `OLLAMA_HOST` (e.g., `export OLLAMA_HOST="http://192.168.1.10:11434"`).
|
|
150
|
+
- **Custom Model:** You can provide a custom model via `--model`:
|
|
151
|
+
```bash
|
|
152
|
+
hey "summarize this file" --model llama3
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
136
157
|
## Security
|
|
137
158
|
|
|
138
159
|
Safety is enforced at runtime via a local governance engine (`~/.hey-rules.json`):
|
|
@@ -45,6 +45,10 @@ brew tap sinsniwal/hey-cli
|
|
|
45
45
|
brew install hey-cli
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
> [!TIP]
|
|
49
|
+
> **Apple Silicon (M1/M2/M3) Note:** If you see an error about `Rosetta 2` while installing via Homebrew, ensure your terminal is running natively (not under Rosetta emulation). You can force a native installation by running:
|
|
50
|
+
> `arch -arm64 brew install hey-cli`
|
|
51
|
+
|
|
48
52
|
### macOS & Linux (curl)
|
|
49
53
|
|
|
50
54
|
```bash
|
|
@@ -105,6 +109,23 @@ hey <your objective in plain English>
|
|
|
105
109
|
|
|
106
110
|
---
|
|
107
111
|
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Authentication & Custom Endpoints
|
|
116
|
+
|
|
117
|
+
`hey` works locally by default, but it also supports authenticated Ollama instances and custom hosts:
|
|
118
|
+
|
|
119
|
+
- **Standard Login:** Most users should run `ollama login` once to authenticate their terminal with the local or cloud instance.
|
|
120
|
+
- **Auth Key:** If you are in a CI/CD or server environment, you can set the `OLLAMA_API_KEY` environment variable.
|
|
121
|
+
- **Custom Host:** If Ollama is running on a different port or machine, set `OLLAMA_HOST` (e.g., `export OLLAMA_HOST="http://192.168.1.10:11434"`).
|
|
122
|
+
- **Custom Model:** You can provide a custom model via `--model`:
|
|
123
|
+
```bash
|
|
124
|
+
hey "summarize this file" --model llama3
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
108
129
|
## Security
|
|
109
130
|
|
|
110
131
|
Safety is enforced at runtime via a local governance engine (`~/.hey-rules.json`):
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import urllib.request
|
|
5
|
+
import urllib.error
|
|
6
|
+
|
|
7
|
+
from .governance import GovernanceEngine
|
|
8
|
+
from .llm import generate_command
|
|
9
|
+
from .history import HistoryManager
|
|
10
|
+
from .runner import CommandRunner
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_ollama():
|
|
19
|
+
"""Check if Ollama is reachable. Exit with instructions if not."""
|
|
20
|
+
from .llm import OLLAMA_HOST
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
urllib.request.urlopen(OLLAMA_HOST, timeout=2)
|
|
24
|
+
except Exception:
|
|
25
|
+
msg = Text()
|
|
26
|
+
msg.append("Ollama is not running or not installed.\n\n", style="bold red")
|
|
27
|
+
msg.append("1. Install Ollama:\n", style="bold white")
|
|
28
|
+
msg.append(" Linux / macOS:\n", style="dim")
|
|
29
|
+
msg.append(
|
|
30
|
+
" curl -fsSL https://ollama.com/install.sh | sh\n", style="bold cyan"
|
|
31
|
+
)
|
|
32
|
+
msg.append(" Windows:\n", style="dim")
|
|
33
|
+
msg.append(" https://ollama.com/download/windows\n\n", style="bold cyan")
|
|
34
|
+
msg.append("2. Authenticate:\n", style="bold white")
|
|
35
|
+
msg.append(" ollama login\n\n", style="bold cyan")
|
|
36
|
+
msg.append("3. Pull the default model:\n", style="bold white")
|
|
37
|
+
msg.append(" ollama pull gpt-oss:20b-cloud", style="bold cyan")
|
|
38
|
+
console.print(
|
|
39
|
+
Panel(
|
|
40
|
+
msg,
|
|
41
|
+
title="[bold yellow]⚠ Ollama Required[/bold yellow]",
|
|
42
|
+
border_style="yellow",
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
parser = argparse.ArgumentParser(
|
|
50
|
+
description="hey-cli: a secure, zero-bloat CLI companion.",
|
|
51
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
parser.add_argument("objective", nargs="*", help="Goal or task description")
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--level",
|
|
57
|
+
type=int,
|
|
58
|
+
choices=[0, 1, 2, 3],
|
|
59
|
+
default=None,
|
|
60
|
+
help="0: Dry-Run\n1: Supervised (Default)\n2: Unrestricted (Danger)\n3: Troubleshooter",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--init", action="store_true", help="Initialize ~/.hey-rules.json"
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--clear", action="store_true", help="Clear conversational memory history"
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--check-cache", type=str, help="Check local cache for instant fix"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
args = parser.parse_args()
|
|
73
|
+
|
|
74
|
+
gov = GovernanceEngine()
|
|
75
|
+
history_mgr = HistoryManager()
|
|
76
|
+
|
|
77
|
+
if args.clear:
|
|
78
|
+
history_mgr.clear()
|
|
79
|
+
console.print("[dim]Conversational history wiped clean.[/dim]")
|
|
80
|
+
sys.exit(0)
|
|
81
|
+
|
|
82
|
+
active_level = args.level
|
|
83
|
+
if active_level is None:
|
|
84
|
+
active_level = gov.rules.get("config", {}).get("default_level", 1)
|
|
85
|
+
|
|
86
|
+
model_name = gov.rules.get("config", {}).get("model", "gpt-oss:20b-cloud")
|
|
87
|
+
|
|
88
|
+
if args.init:
|
|
89
|
+
if gov.init_rules():
|
|
90
|
+
console.print(f"Initialized security rules at {gov.rules_path}")
|
|
91
|
+
else:
|
|
92
|
+
console.print(f"Rules already exist at {gov.rules_path}")
|
|
93
|
+
sys.exit(0)
|
|
94
|
+
|
|
95
|
+
if args.check_cache:
|
|
96
|
+
sys.exit(0)
|
|
97
|
+
|
|
98
|
+
# Only check Ollama when we're about to call the LLM
|
|
99
|
+
check_ollama()
|
|
100
|
+
|
|
101
|
+
piped_data = ""
|
|
102
|
+
if not sys.stdin.isatty():
|
|
103
|
+
try:
|
|
104
|
+
piped_data = sys.stdin.read()
|
|
105
|
+
sys.stdin = open("/dev/tty")
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
objective = " ".join(args.objective).strip()
|
|
110
|
+
if not objective and not piped_data:
|
|
111
|
+
parser.print_help()
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
# Build complete user message for saving later
|
|
115
|
+
user_prompt = objective
|
|
116
|
+
if piped_data:
|
|
117
|
+
user_prompt += f"\n\n[Piped Data]:\n{piped_data}"
|
|
118
|
+
|
|
119
|
+
console.print("[bold yellow]●[/bold yellow] Thinking...")
|
|
120
|
+
past_messages = history_mgr.load()
|
|
121
|
+
try:
|
|
122
|
+
response = generate_command(
|
|
123
|
+
objective, context=piped_data, model_name=model_name, history=past_messages
|
|
124
|
+
)
|
|
125
|
+
except urllib.error.HTTPError as e:
|
|
126
|
+
if e.code == 401:
|
|
127
|
+
msg = Text()
|
|
128
|
+
msg.append("Ollama authentication required.\n\n", style="bold red")
|
|
129
|
+
msg.append("Your connection to Ollama is not authenticated.\n", style="bold white")
|
|
130
|
+
msg.append("\nPlease run: ", style="dim")
|
|
131
|
+
msg.append("ollama login\n", style="bold cyan")
|
|
132
|
+
msg.append("\nThis will verify your identity with Ollama and allow the request to proceed.", style="dim")
|
|
133
|
+
console.print(Panel(msg, title="[bold yellow]🔑 Authentication Required[/bold yellow]", border_style="yellow"))
|
|
134
|
+
else:
|
|
135
|
+
console.print(f"\n[bold red]● Ollama API error:[/bold red] HTTP {e.code} — {e.reason}")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
except (urllib.error.URLError, ConnectionError, OSError):
|
|
138
|
+
check_ollama() # shows the panel and exits
|
|
139
|
+
sys.exit(1) # fallback — should never reach here
|
|
140
|
+
except Exception as e:
|
|
141
|
+
console.print(f"\n[bold red]● Error:[/bold red] {e}")
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
# Save the user query to history IMMEDIATELY
|
|
145
|
+
history_mgr.append("user", user_prompt)
|
|
146
|
+
|
|
147
|
+
runner = CommandRunner(
|
|
148
|
+
governance=gov,
|
|
149
|
+
level=active_level,
|
|
150
|
+
model_name=model_name,
|
|
151
|
+
history_mgr=history_mgr,
|
|
152
|
+
)
|
|
153
|
+
runner.execute_flow(response, objective)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
main()
|
|
@@ -6,6 +6,8 @@ import urllib.error
|
|
|
6
6
|
from .models import CommandResponse, TroubleshootResponse
|
|
7
7
|
|
|
8
8
|
DEFAULT_MODEL = "gpt-oss:20b-cloud"
|
|
9
|
+
OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
|
|
10
|
+
OLLAMA_API_KEY = os.environ.get("OLLAMA_API_KEY", "")
|
|
9
11
|
|
|
10
12
|
SYSTEM_PROMPT = r"""You are hey-cli, an autonomous, minimalist CLI companion and terminal expert.
|
|
11
13
|
Your primary goal is to turn natural language objectives and error logs into actionable shell commands.
|
|
@@ -26,33 +28,41 @@ CRITICAL JSON REQUIREMENT: If your bash command contains any backslashes (e.g. f
|
|
|
26
28
|
|
|
27
29
|
from .skills import get_compiled_skills
|
|
28
30
|
|
|
31
|
+
|
|
29
32
|
def get_system_context() -> str:
|
|
30
33
|
os_name = platform.system()
|
|
31
34
|
os_release = platform.release()
|
|
32
35
|
arch = platform.machine()
|
|
33
36
|
shell = os.environ.get("SHELL", "unknown")
|
|
34
|
-
|
|
37
|
+
|
|
35
38
|
skills_block = f"\n\n{get_compiled_skills()}"
|
|
36
|
-
|
|
39
|
+
|
|
37
40
|
return f"Operating System: {os_name} {os_release} ({arch})\nCurrent Shell: {shell}{skills_block}"
|
|
38
41
|
|
|
42
|
+
|
|
39
43
|
TROUBLESHOOT_PROMPT = r"""You are acting as an iterative troubleshooter.
|
|
40
44
|
You will be provided with an objective, the previous commands attempted, and the stdout/stderr.
|
|
41
45
|
Determine the next command to run to resolve the issue, OR if the issue is resolved, indicate it.
|
|
42
46
|
Keep your explanation brief and chill. If a file or tests do not exist, do not try to aggressively brute-force create configurations. Just explain the situation and set is_resolved=True to gracefully stop.
|
|
43
47
|
"""
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
|
|
50
|
+
def generate_command(
|
|
51
|
+
prompt: str,
|
|
52
|
+
context: str = "",
|
|
53
|
+
model_name: str = DEFAULT_MODEL,
|
|
54
|
+
history: list = None,
|
|
55
|
+
) -> CommandResponse:
|
|
46
56
|
content = prompt
|
|
47
57
|
if context:
|
|
48
58
|
content = f"Context (e.g. error logs or piped data):\n{context}\n\nObjective:\n{prompt}"
|
|
49
|
-
|
|
59
|
+
|
|
50
60
|
sys_context = f"--- ENVIRONMENT ---\n{get_system_context()}\n-------------------\n"
|
|
51
61
|
msgs = [{"role": "system", "content": SYSTEM_PROMPT + "\n\n" + sys_context}]
|
|
52
62
|
if history:
|
|
53
63
|
msgs.extend(history)
|
|
54
64
|
msgs.append({"role": "user", "content": content})
|
|
55
|
-
|
|
65
|
+
|
|
56
66
|
max_retries = 3
|
|
57
67
|
last_error = None
|
|
58
68
|
raw_val = "None"
|
|
@@ -64,16 +74,20 @@ def generate_command(prompt: str, context: str = "", model_name: str = DEFAULT_M
|
|
|
64
74
|
"messages": msgs,
|
|
65
75
|
"format": "json",
|
|
66
76
|
"stream": False,
|
|
67
|
-
"options": {"temperature": 0.0}
|
|
77
|
+
"options": {"temperature": 0.0},
|
|
68
78
|
}
|
|
79
|
+
url = f"{OLLAMA_HOST.rstrip('/')}/api/chat"
|
|
80
|
+
headers = {"Content-Type": "application/json"}
|
|
81
|
+
if OLLAMA_API_KEY:
|
|
82
|
+
headers["Authorization"] = f"Bearer {OLLAMA_API_KEY}"
|
|
69
83
|
req = urllib.request.Request(
|
|
70
|
-
|
|
71
|
-
data=json.dumps(payload).encode(
|
|
72
|
-
headers=
|
|
84
|
+
url,
|
|
85
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
86
|
+
headers=headers,
|
|
73
87
|
)
|
|
74
88
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
75
|
-
response = json.loads(resp.read().decode(
|
|
76
|
-
|
|
89
|
+
response = json.loads(resp.read().decode("utf-8"))
|
|
90
|
+
|
|
77
91
|
raw_val = response["message"]["content"]
|
|
78
92
|
content_str = raw_val
|
|
79
93
|
|
|
@@ -81,10 +95,10 @@ def generate_command(prompt: str, context: str = "", model_name: str = DEFAULT_M
|
|
|
81
95
|
content_str = content_str[7:-3].strip()
|
|
82
96
|
elif content_str.startswith("```"):
|
|
83
97
|
content_str = content_str[3:-3].strip()
|
|
84
|
-
|
|
98
|
+
|
|
85
99
|
data = json.loads(content_str)
|
|
86
100
|
return CommandResponse(**data)
|
|
87
|
-
|
|
101
|
+
|
|
88
102
|
except (urllib.error.URLError, ConnectionError, OSError):
|
|
89
103
|
raise # Ollama not reachable — don't retry, bubble up to cli
|
|
90
104
|
except Exception as e:
|
|
@@ -93,31 +107,48 @@ def generate_command(prompt: str, context: str = "", model_name: str = DEFAULT_M
|
|
|
93
107
|
return CommandResponse(
|
|
94
108
|
command="",
|
|
95
109
|
explanation=f"LLM Safety Trigger: The model refused to generate this command.\n\nRaw output: {raw_val.strip()}",
|
|
96
|
-
needs_context=False
|
|
110
|
+
needs_context=False,
|
|
97
111
|
)
|
|
98
|
-
|
|
112
|
+
|
|
99
113
|
msgs.append({"role": "assistant", "content": raw_val})
|
|
100
|
-
msgs.append(
|
|
114
|
+
msgs.append(
|
|
115
|
+
{
|
|
116
|
+
"role": "user",
|
|
117
|
+
"content": f"Your JSON output failed validation: {str(e)}\nPlease strictly follow the schema and output ONLY valid JSON without markdown wrapping.",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
101
120
|
|
|
102
121
|
return CommandResponse(
|
|
103
|
-
command="",
|
|
104
|
-
explanation=f"Error generating command from LLM after {max_retries} retries: {str(last_error)}\nRaw Output:\n{raw_val}"
|
|
122
|
+
command="",
|
|
123
|
+
explanation=f"Error generating command from LLM after {max_retries} retries: {str(last_error)}\nRaw Output:\n{raw_val}",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def generate_troubleshoot_step(
|
|
128
|
+
objective: str, history: list, model_name: str = DEFAULT_MODEL
|
|
129
|
+
) -> TroubleshootResponse:
|
|
130
|
+
history_text = "\n".join(
|
|
131
|
+
[
|
|
132
|
+
f"Cmd: {h['cmd']}\nExit: {h['exit_code']}\nOut/Err:\n{h['output']}"
|
|
133
|
+
for h in history
|
|
134
|
+
]
|
|
105
135
|
)
|
|
106
136
|
|
|
107
|
-
def generate_troubleshoot_step(objective: str, history: list, model_name: str = DEFAULT_MODEL) -> TroubleshootResponse:
|
|
108
|
-
history_text = "\n".join([
|
|
109
|
-
f"Cmd: {h['cmd']}\nExit: {h['exit_code']}\nOut/Err:\n{h['output']}"
|
|
110
|
-
for h in history
|
|
111
|
-
])
|
|
112
|
-
|
|
113
137
|
content = f"Objective:\n{objective}\n\nHistory of execution:\n{history_text}\n\nAnalyze the specific error and provide the NEXT logical command to test or fix. Re-read logs carefully."
|
|
114
|
-
|
|
138
|
+
|
|
115
139
|
sys_context = f"--- ENVIRONMENT ---\n{get_system_context()}\n-------------------\n"
|
|
116
140
|
msgs = [
|
|
117
|
-
{
|
|
118
|
-
|
|
141
|
+
{
|
|
142
|
+
"role": "system",
|
|
143
|
+
"content": SYSTEM_PROMPT
|
|
144
|
+
+ "\n"
|
|
145
|
+
+ TROUBLESHOOT_PROMPT
|
|
146
|
+
+ "\n\n"
|
|
147
|
+
+ sys_context,
|
|
148
|
+
},
|
|
149
|
+
{"role": "user", "content": content},
|
|
119
150
|
]
|
|
120
|
-
|
|
151
|
+
|
|
121
152
|
max_retries = 3
|
|
122
153
|
last_error = None
|
|
123
154
|
raw_val = "None"
|
|
@@ -129,38 +160,47 @@ def generate_troubleshoot_step(objective: str, history: list, model_name: str =
|
|
|
129
160
|
"messages": msgs,
|
|
130
161
|
"format": "json",
|
|
131
162
|
"stream": False,
|
|
132
|
-
"options": {"temperature": 0.0}
|
|
163
|
+
"options": {"temperature": 0.0},
|
|
133
164
|
}
|
|
165
|
+
url = f"{OLLAMA_HOST.rstrip('/')}/api/chat"
|
|
166
|
+
headers = {"Content-Type": "application/json"}
|
|
167
|
+
if OLLAMA_API_KEY:
|
|
168
|
+
headers["Authorization"] = f"Bearer {OLLAMA_API_KEY}"
|
|
134
169
|
req = urllib.request.Request(
|
|
135
|
-
|
|
136
|
-
data=json.dumps(payload).encode(
|
|
137
|
-
headers=
|
|
170
|
+
url,
|
|
171
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
172
|
+
headers=headers,
|
|
138
173
|
)
|
|
139
174
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
140
|
-
response = json.loads(resp.read().decode(
|
|
141
|
-
|
|
175
|
+
response = json.loads(resp.read().decode("utf-8"))
|
|
176
|
+
|
|
142
177
|
raw_val = response["message"]["content"].strip()
|
|
143
178
|
if not raw_val:
|
|
144
179
|
raise ValueError("LLM returned empty JSON object.")
|
|
145
|
-
|
|
180
|
+
|
|
146
181
|
content_str = raw_val
|
|
147
182
|
if content_str.startswith("```json"):
|
|
148
183
|
content_str = content_str[7:-3].strip()
|
|
149
184
|
elif content_str.startswith("```"):
|
|
150
185
|
content_str = content_str[3:-3].strip()
|
|
151
|
-
|
|
186
|
+
|
|
152
187
|
data = json.loads(content_str)
|
|
153
188
|
return TroubleshootResponse(**data)
|
|
154
|
-
|
|
189
|
+
|
|
155
190
|
except (urllib.error.URLError, ConnectionError, OSError):
|
|
156
191
|
raise # Ollama not reachable — don't retry, bubble up to cli
|
|
157
192
|
except Exception as e:
|
|
158
193
|
last_error = e
|
|
159
194
|
msgs.append({"role": "assistant", "content": raw_val})
|
|
160
|
-
msgs.append(
|
|
195
|
+
msgs.append(
|
|
196
|
+
{
|
|
197
|
+
"role": "user",
|
|
198
|
+
"content": f"Your JSON output failed validation: {str(e)}\nFix the syntax and output ONLY strict JSON schema.",
|
|
199
|
+
}
|
|
200
|
+
)
|
|
161
201
|
|
|
162
202
|
return TroubleshootResponse(
|
|
163
203
|
command=None,
|
|
164
204
|
explanation=f"Error analyzing execution after {max_retries} retries: {str(last_error)}\nRaw Output:\n{raw_val}",
|
|
165
|
-
is_resolved=False
|
|
205
|
+
is_resolved=False,
|
|
166
206
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hey-cli-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.8
|
|
4
4
|
Summary: A secure, zero-bloat CLI companion that turns natural language and error logs into executable commands.
|
|
5
5
|
Author: Mohit Singh Sinsniwal
|
|
6
6
|
Project-URL: Homepage, https://github.com/sinsniwal/hey-cli
|
|
@@ -73,6 +73,10 @@ brew tap sinsniwal/hey-cli
|
|
|
73
73
|
brew install hey-cli
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
> [!TIP]
|
|
77
|
+
> **Apple Silicon (M1/M2/M3) Note:** If you see an error about `Rosetta 2` while installing via Homebrew, ensure your terminal is running natively (not under Rosetta emulation). You can force a native installation by running:
|
|
78
|
+
> `arch -arm64 brew install hey-cli`
|
|
79
|
+
|
|
76
80
|
### macOS & Linux (curl)
|
|
77
81
|
|
|
78
82
|
```bash
|
|
@@ -133,6 +137,23 @@ hey <your objective in plain English>
|
|
|
133
137
|
|
|
134
138
|
---
|
|
135
139
|
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Authentication & Custom Endpoints
|
|
144
|
+
|
|
145
|
+
`hey` works locally by default, but it also supports authenticated Ollama instances and custom hosts:
|
|
146
|
+
|
|
147
|
+
- **Standard Login:** Most users should run `ollama login` once to authenticate their terminal with the local or cloud instance.
|
|
148
|
+
- **Auth Key:** If you are in a CI/CD or server environment, you can set the `OLLAMA_API_KEY` environment variable.
|
|
149
|
+
- **Custom Host:** If Ollama is running on a different port or machine, set `OLLAMA_HOST` (e.g., `export OLLAMA_HOST="http://192.168.1.10:11434"`).
|
|
150
|
+
- **Custom Model:** You can provide a custom model via `--model`:
|
|
151
|
+
```bash
|
|
152
|
+
hey "summarize this file" --model llama3
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
136
157
|
## Security
|
|
137
158
|
|
|
138
159
|
Safety is enforced at runtime via a local governance engine (`~/.hey-rules.json`):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hey-cli-python"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.8"
|
|
8
8
|
description = "A secure, zero-bloat CLI companion that turns natural language and error logs into executable commands."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import sys
|
|
3
|
-
import os
|
|
4
|
-
import urllib.request
|
|
5
|
-
import urllib.error
|
|
6
|
-
|
|
7
|
-
from .governance import GovernanceEngine
|
|
8
|
-
from .llm import generate_command
|
|
9
|
-
from .history import HistoryManager
|
|
10
|
-
from .runner import CommandRunner
|
|
11
|
-
from rich.console import Console
|
|
12
|
-
from rich.panel import Panel
|
|
13
|
-
from rich.text import Text
|
|
14
|
-
|
|
15
|
-
console = Console()
|
|
16
|
-
|
|
17
|
-
def check_ollama():
|
|
18
|
-
"""Check if Ollama is reachable at localhost:11434. Exit with instructions if not."""
|
|
19
|
-
try:
|
|
20
|
-
urllib.request.urlopen("http://localhost:11434", timeout=2)
|
|
21
|
-
except Exception:
|
|
22
|
-
msg = Text()
|
|
23
|
-
msg.append("Ollama is not running or not installed.\n\n", style="bold red")
|
|
24
|
-
msg.append("Install Ollama:\n", style="bold white")
|
|
25
|
-
msg.append(" Linux / macOS:\n", style="dim")
|
|
26
|
-
msg.append(" curl -fsSL https://ollama.com/install.sh | sh\n", style="bold cyan")
|
|
27
|
-
msg.append(" Windows:\n", style="dim")
|
|
28
|
-
msg.append(" https://ollama.com/download/windows\n\n", style="bold cyan")
|
|
29
|
-
msg.append("Then pull the default model:\n", style="bold white")
|
|
30
|
-
msg.append(" ollama pull gpt-oss:20b-cloud", style="bold cyan")
|
|
31
|
-
console.print(Panel(msg, title="[bold yellow]⚠ Ollama Required[/bold yellow]", border_style="yellow"))
|
|
32
|
-
sys.exit(1)
|
|
33
|
-
|
|
34
|
-
def main():
|
|
35
|
-
parser = argparse.ArgumentParser(
|
|
36
|
-
description="hey-cli: a secure, zero-bloat CLI companion.",
|
|
37
|
-
formatter_class=argparse.RawTextHelpFormatter
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
parser.add_argument("objective", nargs="*", help="Goal or task description")
|
|
41
|
-
parser.add_argument("--level", type=int, choices=[0, 1, 2, 3], default=None,
|
|
42
|
-
help="0: Dry-Run\n1: Supervised (Default)\n2: Unrestricted (Danger)\n3: Troubleshooter")
|
|
43
|
-
parser.add_argument("--init", action="store_true", help="Initialize ~/.hey-rules.json")
|
|
44
|
-
parser.add_argument("--clear", action="store_true", help="Clear conversational memory history")
|
|
45
|
-
parser.add_argument("--check-cache", type=str, help="Check local cache for instant fix")
|
|
46
|
-
|
|
47
|
-
args = parser.parse_args()
|
|
48
|
-
|
|
49
|
-
gov = GovernanceEngine()
|
|
50
|
-
history_mgr = HistoryManager()
|
|
51
|
-
|
|
52
|
-
if args.clear:
|
|
53
|
-
history_mgr.clear()
|
|
54
|
-
console.print("[dim]Conversational history wiped clean.[/dim]")
|
|
55
|
-
sys.exit(0)
|
|
56
|
-
|
|
57
|
-
active_level = args.level
|
|
58
|
-
if active_level is None:
|
|
59
|
-
active_level = gov.rules.get("config", {}).get("default_level", 1)
|
|
60
|
-
|
|
61
|
-
model_name = gov.rules.get("config", {}).get("model", "gpt-oss:20b-cloud")
|
|
62
|
-
|
|
63
|
-
if args.init:
|
|
64
|
-
if gov.init_rules():
|
|
65
|
-
console.print(f"Initialized security rules at {gov.rules_path}")
|
|
66
|
-
else:
|
|
67
|
-
console.print(f"Rules already exist at {gov.rules_path}")
|
|
68
|
-
sys.exit(0)
|
|
69
|
-
|
|
70
|
-
if args.check_cache:
|
|
71
|
-
sys.exit(0)
|
|
72
|
-
|
|
73
|
-
# Only check Ollama when we're about to call the LLM
|
|
74
|
-
check_ollama()
|
|
75
|
-
|
|
76
|
-
piped_data = ""
|
|
77
|
-
if not sys.stdin.isatty():
|
|
78
|
-
try:
|
|
79
|
-
piped_data = sys.stdin.read()
|
|
80
|
-
sys.stdin = open('/dev/tty')
|
|
81
|
-
except Exception:
|
|
82
|
-
pass
|
|
83
|
-
|
|
84
|
-
objective = " ".join(args.objective).strip()
|
|
85
|
-
if not objective and not piped_data:
|
|
86
|
-
parser.print_help()
|
|
87
|
-
sys.exit(1)
|
|
88
|
-
|
|
89
|
-
# Build complete user message for saving later
|
|
90
|
-
user_prompt = objective
|
|
91
|
-
if piped_data:
|
|
92
|
-
user_prompt += f"\n\n[Piped Data]:\n{piped_data}"
|
|
93
|
-
|
|
94
|
-
console.print("[bold yellow]●[/bold yellow] Thinking...")
|
|
95
|
-
past_messages = history_mgr.load()
|
|
96
|
-
try:
|
|
97
|
-
response = generate_command(objective, context=piped_data, model_name=model_name, history=past_messages)
|
|
98
|
-
except (urllib.error.URLError, ConnectionError, OSError):
|
|
99
|
-
check_ollama() # shows the panel and exits
|
|
100
|
-
sys.exit(1) # fallback — should never reach here
|
|
101
|
-
|
|
102
|
-
# Save the user query to history IMMEDIATELY
|
|
103
|
-
history_mgr.append("user", user_prompt)
|
|
104
|
-
|
|
105
|
-
runner = CommandRunner(governance=gov, level=active_level, model_name=model_name, history_mgr=history_mgr)
|
|
106
|
-
runner.execute_flow(response, objective)
|
|
107
|
-
|
|
108
|
-
if __name__ == "__main__":
|
|
109
|
-
main()
|
|
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
|
|
File without changes
|