toni-cli 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.
- toni_cli-0.1.0/LICENSE +21 -0
- toni_cli-0.1.0/PKG-INFO +16 -0
- toni_cli-0.1.0/pyproject.toml +28 -0
- toni_cli-0.1.0/setup.cfg +4 -0
- toni_cli-0.1.0/setup.py +5 -0
- toni_cli-0.1.0/src/toni/__init__.py +3 -0
- toni_cli-0.1.0/src/toni/cli.py +89 -0
- toni_cli-0.1.0/src/toni/core.py +161 -0
- toni_cli-0.1.0/src/toni_cli.egg-info/PKG-INFO +16 -0
- toni_cli-0.1.0/src/toni_cli.egg-info/SOURCES.txt +13 -0
- toni_cli-0.1.0/src/toni_cli.egg-info/dependency_links.txt +1 -0
- toni_cli-0.1.0/src/toni_cli.egg-info/entry_points.txt +2 -0
- toni_cli-0.1.0/src/toni_cli.egg-info/requires.txt +2 -0
- toni_cli-0.1.0/src/toni_cli.egg-info/top_level.txt +1 -0
- toni_cli-0.1.0/tests/test_toni.py +28 -0
toni_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 MatrixDynamo
|
|
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.
|
toni_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: toni-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal Operation Natural Instruction - AI-powered terminal assistant
|
|
5
|
+
Author-email: Dakai Ou <oudakai@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dakai/toni
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/dakai/toni/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: openai>=1.0.0
|
|
16
|
+
Requires-Dist: google-generativeai>=0.1.0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "toni-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Terminal Operation Natural Instruction - AI-powered terminal assistant"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "Dakai Ou", email = "oudakai@gmail.com"}]
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
]
|
|
17
|
+
requires-python = ">=3.7"
|
|
18
|
+
dependencies = [
|
|
19
|
+
"openai>=1.0.0",
|
|
20
|
+
"google-generativeai>=0.1.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
"Homepage" = "https://github.com/dakai/toni"
|
|
25
|
+
"Bug Tracker" = "https://github.com/dakai/toni/issues"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
toni = "toni.cli:main"
|
toni_cli-0.1.0/setup.cfg
ADDED
toni_cli-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from toni.core import (
|
|
6
|
+
get_system_info,
|
|
7
|
+
get_gemini_response,
|
|
8
|
+
get_open_ai_response,
|
|
9
|
+
command_exists,
|
|
10
|
+
execute_command,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
try:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description="TONI: Terminal Operation Natural Instruction"
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument("query", nargs="+", help="Your natural language query")
|
|
20
|
+
args = parser.parse_args()
|
|
21
|
+
|
|
22
|
+
# Remove trailing question mark if present
|
|
23
|
+
query = " ".join(args.query).rstrip("?")
|
|
24
|
+
|
|
25
|
+
system_info = get_system_info()
|
|
26
|
+
print(f"Detected system: {system_info}")
|
|
27
|
+
|
|
28
|
+
google_api_key = os.environ.get("GOOGLEAI_API_KEY")
|
|
29
|
+
openai_api_key = os.environ.get("OPENAI_API_KEY")
|
|
30
|
+
response = None
|
|
31
|
+
|
|
32
|
+
# Try Gemini first, fall back to OpenAI
|
|
33
|
+
if google_api_key:
|
|
34
|
+
response = get_gemini_response(google_api_key, query, system_info)
|
|
35
|
+
|
|
36
|
+
if response is None and openai_api_key:
|
|
37
|
+
response = get_open_ai_response(openai_api_key, query, system_info)
|
|
38
|
+
|
|
39
|
+
if response is None:
|
|
40
|
+
print(
|
|
41
|
+
"Please set the GOOGLEAI_API_KEY or OPENAI_API_KEY environment variable."
|
|
42
|
+
)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(response)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"An error occurred while parsing the response: {e}")
|
|
49
|
+
print(f"Raw response: {response}")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if data.get("exec") == False:
|
|
53
|
+
print(data.get("exp"))
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
cmd = data.get("cmd")
|
|
57
|
+
|
|
58
|
+
# Check if the command exists
|
|
59
|
+
if cmd and not command_exists(cmd):
|
|
60
|
+
print(
|
|
61
|
+
f"Warning: The command '{cmd.split()[0]}' doesn't appear to be installed on your system."
|
|
62
|
+
)
|
|
63
|
+
print(f"Suggested command: {cmd}")
|
|
64
|
+
print(f"Explanation: {data.get('exp')}")
|
|
65
|
+
print("Please verify that this command will work on your system.")
|
|
66
|
+
else:
|
|
67
|
+
print(f"Suggested command: {cmd}")
|
|
68
|
+
print(f"Explanation: {data.get('exp')}")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
confirmation = input("Do you want to execute this command? (y/n): ").lower()
|
|
72
|
+
if confirmation == "y" or confirmation == "":
|
|
73
|
+
execute_command(cmd)
|
|
74
|
+
else:
|
|
75
|
+
print("Command execution cancelled.")
|
|
76
|
+
except KeyboardInterrupt:
|
|
77
|
+
print("\nOperation cancelled by user.")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
except KeyboardInterrupt:
|
|
81
|
+
print("\nOperation cancelled by user.")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
try:
|
|
87
|
+
main()
|
|
88
|
+
except KeyboardInterrupt:
|
|
89
|
+
print("\nOperation cancelled by user.")
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from openai import OpenAI
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
from google import genai
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
system_message = """Your are a powerful terminal assistant generating a JSON containing a command line for my input.
|
|
10
|
+
You will always reply using the following json structure: {{"cmd":"the command", "exp": "some explanation", "exec": true}}.
|
|
11
|
+
Your answer will always only contain the json structure, never add any advice or supplementary detail or information,
|
|
12
|
+
even if I asked the same question before.
|
|
13
|
+
The field cmd will contain a single line command (don't use new lines, use separators like && and ; instead).
|
|
14
|
+
The field exp will contain an short explanation of the command if you managed to generate an executable command, otherwise it will contain the reason of your failure.
|
|
15
|
+
The field exec will contain true if you managed to generate an executable command, false otherwise.
|
|
16
|
+
|
|
17
|
+
The host system is using {system_info}. Please ensure commands are compatible with this environment.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
Me: list all files in my home dir
|
|
21
|
+
You: {{"cmd":"ls ~", "exp": "list all files in your home dir", "exec": true}}
|
|
22
|
+
Me: list all pods of all namespaces
|
|
23
|
+
You: {{"cmd":"kubectl get pods --all-namespaces", "exp": "list pods form all k8s namespaces", "exec": true}}
|
|
24
|
+
Me: how are you ?
|
|
25
|
+
You: {{"cmd":"", "exp": "I'm good thanks but I cannot generate a command for this.", "exec": false}}"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_system_info():
|
|
29
|
+
system = platform.system()
|
|
30
|
+
if system == "Linux":
|
|
31
|
+
try:
|
|
32
|
+
distro = (
|
|
33
|
+
subprocess.check_output("cat /etc/os-release | grep -w ID", shell=True)
|
|
34
|
+
.decode()
|
|
35
|
+
.strip()
|
|
36
|
+
.split("=")[1]
|
|
37
|
+
.strip('"')
|
|
38
|
+
)
|
|
39
|
+
return f"Linux ({distro})"
|
|
40
|
+
except:
|
|
41
|
+
return "Linux"
|
|
42
|
+
elif system == "Darwin":
|
|
43
|
+
return "macOS"
|
|
44
|
+
elif system == "Windows":
|
|
45
|
+
return "Windows"
|
|
46
|
+
else:
|
|
47
|
+
return system
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_gemini_response(api_key, prompt, system_info):
|
|
51
|
+
try:
|
|
52
|
+
client = genai.Client(api_key=api_key)
|
|
53
|
+
|
|
54
|
+
formatted_system_message = system_message.format(system_info=system_info)
|
|
55
|
+
|
|
56
|
+
# Create the generation config with system instructions
|
|
57
|
+
# generation_config = {
|
|
58
|
+
# "temperature": 0.2,
|
|
59
|
+
# "top_p": 0.95,
|
|
60
|
+
# "top_k": 0,
|
|
61
|
+
# "max_output_tokens": 1024,
|
|
62
|
+
# }
|
|
63
|
+
|
|
64
|
+
# The new Gemini API doesn't always handle system messages properly
|
|
65
|
+
# Let's combine the system message with the user prompt
|
|
66
|
+
combined_prompt = f"{formatted_system_message}\n\nUser request: {prompt}"
|
|
67
|
+
|
|
68
|
+
response = client.models.generate_content(
|
|
69
|
+
model="gemini-2.0-flash",
|
|
70
|
+
contents=[{"parts": [{"text": combined_prompt}]}],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Extract just the JSON part from the response
|
|
74
|
+
response_text = response.text
|
|
75
|
+
# Find JSON between curly braces if there's extra text
|
|
76
|
+
import re
|
|
77
|
+
|
|
78
|
+
if response_text:
|
|
79
|
+
json_match = re.search(r"(\{.*?\})", response_text, re.DOTALL)
|
|
80
|
+
if json_match:
|
|
81
|
+
return json_match.group(1)
|
|
82
|
+
return response_text
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"An error occurred with Gemini: {e}")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_open_ai_response(api_key, prompt, system_info):
|
|
90
|
+
try:
|
|
91
|
+
client = OpenAI(api_key=api_key)
|
|
92
|
+
|
|
93
|
+
formatted_system_message = system_message.format(system_info=system_info)
|
|
94
|
+
|
|
95
|
+
chat_completion = client.chat.completions.create(
|
|
96
|
+
messages=[
|
|
97
|
+
{"role": "system", "content": formatted_system_message},
|
|
98
|
+
{"role": "user", "content": prompt},
|
|
99
|
+
],
|
|
100
|
+
model="gpt-4o-mini",
|
|
101
|
+
temperature=0.2,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
response = chat_completion.choices[0].message.content
|
|
105
|
+
|
|
106
|
+
# Extract just the JSON part from the response
|
|
107
|
+
import re
|
|
108
|
+
|
|
109
|
+
if response:
|
|
110
|
+
json_match = re.search(r"(\{.*?\})", response, re.DOTALL)
|
|
111
|
+
if json_match:
|
|
112
|
+
return json_match.group(1)
|
|
113
|
+
return response
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
print(f"An error occurred with OpenAI: {e}")
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def write_to_zsh_history(command):
|
|
121
|
+
try:
|
|
122
|
+
current_time = int(time.time()) # Get current Unix timestamp
|
|
123
|
+
timestamped_command = (
|
|
124
|
+
f": {current_time}:0;{command}" # Assuming duration of 0 for now
|
|
125
|
+
)
|
|
126
|
+
with open("/home/dakai/.zsh_history", "a") as f:
|
|
127
|
+
f.write(timestamped_command + "\n")
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"An error occurred while writing to .zsh_history: {e}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def reload_zsh_history():
|
|
133
|
+
try:
|
|
134
|
+
os.system("source ~/.zshrc")
|
|
135
|
+
result = subprocess.run(
|
|
136
|
+
"source ~/.zshrc", shell=True, check=True, text=True, capture_output=True
|
|
137
|
+
)
|
|
138
|
+
print(result.stdout)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
print(f"An error occurred while reloading .zshrc: {e}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def execute_command(command):
|
|
144
|
+
try:
|
|
145
|
+
result = subprocess.run(
|
|
146
|
+
command, shell=True, check=True, text=True, capture_output=True
|
|
147
|
+
)
|
|
148
|
+
print("Command output:")
|
|
149
|
+
print(result.stdout)
|
|
150
|
+
write_to_zsh_history(command)
|
|
151
|
+
# reload_zsh_history()
|
|
152
|
+
except subprocess.CalledProcessError as e:
|
|
153
|
+
print(f"An error occurred while executing the command: {e}")
|
|
154
|
+
print("Error output:")
|
|
155
|
+
print(e.stderr)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def command_exists(command):
|
|
159
|
+
# Extract the base command (before any options or arguments)
|
|
160
|
+
base_command = command.split()[0]
|
|
161
|
+
return shutil.which(base_command) is not None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: toni-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal Operation Natural Instruction - AI-powered terminal assistant
|
|
5
|
+
Author-email: Dakai Ou <oudakai@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dakai/toni
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/dakai/toni/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: openai>=1.0.0
|
|
16
|
+
Requires-Dist: google-generativeai>=0.1.0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/toni/__init__.py
|
|
5
|
+
src/toni/cli.py
|
|
6
|
+
src/toni/core.py
|
|
7
|
+
src/toni_cli.egg-info/PKG-INFO
|
|
8
|
+
src/toni_cli.egg-info/SOURCES.txt
|
|
9
|
+
src/toni_cli.egg-info/dependency_links.txt
|
|
10
|
+
src/toni_cli.egg-info/entry_points.txt
|
|
11
|
+
src/toni_cli.egg-info/requires.txt
|
|
12
|
+
src/toni_cli.egg-info/top_level.txt
|
|
13
|
+
tests/test_toni.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
toni
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Tests for the TONI package."""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
from toni.core import get_system_info, command_exists
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestToni(unittest.TestCase):
|
|
9
|
+
"""Test cases for TONI functionality."""
|
|
10
|
+
|
|
11
|
+
def test_get_system_info(self):
|
|
12
|
+
"""Test that system info is returned."""
|
|
13
|
+
result = get_system_info()
|
|
14
|
+
self.assertIsInstance(result, str)
|
|
15
|
+
self.assertTrue(len(result) > 0)
|
|
16
|
+
|
|
17
|
+
@patch("toni.core.shutil.which")
|
|
18
|
+
def test_command_exists(self, mock_which):
|
|
19
|
+
"""Test command_exists function."""
|
|
20
|
+
mock_which.return_value = "/usr/bin/ls"
|
|
21
|
+
self.assertTrue(command_exists("ls"))
|
|
22
|
+
|
|
23
|
+
mock_which.return_value = None
|
|
24
|
+
self.assertFalse(command_exists("nonexistentcommand"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
unittest.main()
|