hey-cli-python 1.0.5__tar.gz → 1.0.7__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.5 → hey_cli_python-1.0.7}/PKG-INFO +1 -1
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli/cli.py +59 -27
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli/llm.py +81 -41
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli_python.egg-info/PKG-INFO +1 -1
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/pyproject.toml +1 -1
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/LICENSE +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/README.md +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli/__init__.py +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli/governance.py +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli/history.py +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli/models.py +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli/runner.py +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli/skills.py +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli_python.egg-info/SOURCES.txt +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli_python.egg-info/dependency_links.txt +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli_python.egg-info/entry_points.txt +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli_python.egg-info/requires.txt +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/hey_cli_python.egg-info/top_level.txt +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/setup.cfg +0 -0
- {hey_cli_python-1.0.5 → hey_cli_python-1.0.7}/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.7
|
|
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
|
|
@@ -14,59 +14,82 @@ from rich.text import Text
|
|
|
14
14
|
|
|
15
15
|
console = Console()
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
def check_ollama():
|
|
18
|
-
"""Check if Ollama is reachable
|
|
19
|
+
"""Check if Ollama is reachable. Exit with instructions if not."""
|
|
20
|
+
from .llm import OLLAMA_HOST
|
|
21
|
+
|
|
19
22
|
try:
|
|
20
|
-
urllib.request.urlopen(
|
|
23
|
+
urllib.request.urlopen(OLLAMA_HOST, timeout=2)
|
|
21
24
|
except Exception:
|
|
22
25
|
msg = Text()
|
|
23
26
|
msg.append("Ollama is not running or not installed.\n\n", style="bold red")
|
|
24
27
|
msg.append("Install Ollama:\n", style="bold white")
|
|
25
28
|
msg.append(" Linux / macOS:\n", style="dim")
|
|
26
|
-
msg.append(
|
|
29
|
+
msg.append(
|
|
30
|
+
" curl -fsSL https://ollama.com/install.sh | sh\n", style="bold cyan"
|
|
31
|
+
)
|
|
27
32
|
msg.append(" Windows:\n", style="dim")
|
|
28
33
|
msg.append(" https://ollama.com/download/windows\n\n", style="bold cyan")
|
|
29
34
|
msg.append("Then pull the default model:\n", style="bold white")
|
|
30
35
|
msg.append(" ollama pull gpt-oss:20b-cloud", style="bold cyan")
|
|
31
|
-
console.print(
|
|
36
|
+
console.print(
|
|
37
|
+
Panel(
|
|
38
|
+
msg,
|
|
39
|
+
title="[bold yellow]⚠ Ollama Required[/bold yellow]",
|
|
40
|
+
border_style="yellow",
|
|
41
|
+
)
|
|
42
|
+
)
|
|
32
43
|
sys.exit(1)
|
|
33
44
|
|
|
45
|
+
|
|
34
46
|
def main():
|
|
35
47
|
parser = argparse.ArgumentParser(
|
|
36
48
|
description="hey-cli: a secure, zero-bloat CLI companion.",
|
|
37
|
-
formatter_class=argparse.RawTextHelpFormatter
|
|
49
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
38
50
|
)
|
|
39
|
-
|
|
51
|
+
|
|
40
52
|
parser.add_argument("objective", nargs="*", help="Goal or task description")
|
|
41
|
-
parser.add_argument(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--level",
|
|
55
|
+
type=int,
|
|
56
|
+
choices=[0, 1, 2, 3],
|
|
57
|
+
default=None,
|
|
58
|
+
help="0: Dry-Run\n1: Supervised (Default)\n2: Unrestricted (Danger)\n3: Troubleshooter",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--init", action="store_true", help="Initialize ~/.hey-rules.json"
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--clear", action="store_true", help="Clear conversational memory history"
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--check-cache", type=str, help="Check local cache for instant fix"
|
|
68
|
+
)
|
|
69
|
+
|
|
47
70
|
args = parser.parse_args()
|
|
48
|
-
|
|
71
|
+
|
|
49
72
|
gov = GovernanceEngine()
|
|
50
73
|
history_mgr = HistoryManager()
|
|
51
|
-
|
|
74
|
+
|
|
52
75
|
if args.clear:
|
|
53
76
|
history_mgr.clear()
|
|
54
77
|
console.print("[dim]Conversational history wiped clean.[/dim]")
|
|
55
78
|
sys.exit(0)
|
|
56
|
-
|
|
79
|
+
|
|
57
80
|
active_level = args.level
|
|
58
81
|
if active_level is None:
|
|
59
82
|
active_level = gov.rules.get("config", {}).get("default_level", 1)
|
|
60
|
-
|
|
83
|
+
|
|
61
84
|
model_name = gov.rules.get("config", {}).get("model", "gpt-oss:20b-cloud")
|
|
62
|
-
|
|
85
|
+
|
|
63
86
|
if args.init:
|
|
64
87
|
if gov.init_rules():
|
|
65
88
|
console.print(f"Initialized security rules at {gov.rules_path}")
|
|
66
89
|
else:
|
|
67
90
|
console.print(f"Rules already exist at {gov.rules_path}")
|
|
68
91
|
sys.exit(0)
|
|
69
|
-
|
|
92
|
+
|
|
70
93
|
if args.check_cache:
|
|
71
94
|
sys.exit(0)
|
|
72
95
|
|
|
@@ -77,7 +100,7 @@ def main():
|
|
|
77
100
|
if not sys.stdin.isatty():
|
|
78
101
|
try:
|
|
79
102
|
piped_data = sys.stdin.read()
|
|
80
|
-
sys.stdin = open(
|
|
103
|
+
sys.stdin = open("/dev/tty")
|
|
81
104
|
except Exception:
|
|
82
105
|
pass
|
|
83
106
|
|
|
@@ -85,24 +108,33 @@ def main():
|
|
|
85
108
|
if not objective and not piped_data:
|
|
86
109
|
parser.print_help()
|
|
87
110
|
sys.exit(1)
|
|
88
|
-
|
|
111
|
+
|
|
89
112
|
# Build complete user message for saving later
|
|
90
113
|
user_prompt = objective
|
|
91
114
|
if piped_data:
|
|
92
115
|
user_prompt += f"\n\n[Piped Data]:\n{piped_data}"
|
|
93
|
-
|
|
116
|
+
|
|
94
117
|
console.print("[bold yellow]●[/bold yellow] Thinking...")
|
|
95
118
|
past_messages = history_mgr.load()
|
|
96
119
|
try:
|
|
97
|
-
response = generate_command(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
response = generate_command(
|
|
121
|
+
objective, context=piped_data, model_name=model_name, history=past_messages
|
|
122
|
+
)
|
|
123
|
+
except (urllib.error.URLError, ConnectionError, OSError):
|
|
124
|
+
check_ollama() # shows the panel and exits
|
|
125
|
+
sys.exit(1) # fallback — should never reach here
|
|
126
|
+
|
|
101
127
|
# Save the user query to history IMMEDIATELY
|
|
102
128
|
history_mgr.append("user", user_prompt)
|
|
103
|
-
|
|
104
|
-
runner = CommandRunner(
|
|
129
|
+
|
|
130
|
+
runner = CommandRunner(
|
|
131
|
+
governance=gov,
|
|
132
|
+
level=active_level,
|
|
133
|
+
model_name=model_name,
|
|
134
|
+
history_mgr=history_mgr,
|
|
135
|
+
)
|
|
105
136
|
runner.execute_flow(response, objective)
|
|
106
137
|
|
|
138
|
+
|
|
107
139
|
if __name__ == "__main__":
|
|
108
140
|
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,11 +95,11 @@ 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
|
-
|
|
88
|
-
except urllib.error.URLError:
|
|
101
|
+
|
|
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:
|
|
91
105
|
last_error = 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
|
-
|
|
155
|
-
except urllib.error.URLError:
|
|
189
|
+
|
|
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.7
|
|
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
|
|
@@ -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.7"
|
|
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"
|
|
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
|
|
File without changes
|