gitai-local 1.0.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.
- gitai/__init__.py +0 -0
- gitai/config.py +129 -0
- gitai/llm_client.py +355 -0
- gitai/main.py +159 -0
- gitai_local-1.0.0.dist-info/METADATA +226 -0
- gitai_local-1.0.0.dist-info/RECORD +10 -0
- gitai_local-1.0.0.dist-info/WHEEL +5 -0
- gitai_local-1.0.0.dist-info/entry_points.txt +2 -0
- gitai_local-1.0.0.dist-info/licenses/LICENSE +18 -0
- gitai_local-1.0.0.dist-info/top_level.txt +1 -0
gitai/__init__.py
ADDED
|
File without changes
|
gitai/config.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import sys
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from colorama import init, Fore, Style
|
|
8
|
+
|
|
9
|
+
init(autoreset=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
MODEL_DIR = Path(os.environ.get("LOCALAPPDATA", ".")) / "gitai" / "models"
|
|
13
|
+
MODEL_FILENAME = "Qwen2.5.1-Coder-7B-Instruct-Q4_K_M.gguf"
|
|
14
|
+
MODEL_PATH = MODEL_DIR / MODEL_FILENAME
|
|
15
|
+
MODEL_URL = "https://huggingface.co/bartowski/Qwen2.5.1-Coder-7B-Instruct-GGUF/resolve/main/Qwen2.5.1-Coder-7B-Instruct-Q4_K_M.gguf?download=true"
|
|
16
|
+
PORT = 8089
|
|
17
|
+
|
|
18
|
+
def get_secure_env():
|
|
19
|
+
env = os.environ.copy()
|
|
20
|
+
env["LC_ALL"] = "C.UTF-8"
|
|
21
|
+
env["LANG"] = "C.UTF-8"
|
|
22
|
+
return env
|
|
23
|
+
|
|
24
|
+
def model_exists():
|
|
25
|
+
return MODEL_PATH.exists() and MODEL_PATH.stat().st_size > 100_000_000
|
|
26
|
+
|
|
27
|
+
def download_model(on_progress, on_done, on_error):
|
|
28
|
+
import httpx
|
|
29
|
+
def run():
|
|
30
|
+
try:
|
|
31
|
+
MODEL_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
tmp_path = MODEL_PATH.with_suffix(".tmp")
|
|
33
|
+
|
|
34
|
+
with httpx.stream("GET", MODEL_URL, follow_redirects=True, timeout=None) as r:
|
|
35
|
+
r.raise_for_status()
|
|
36
|
+
total = int(r.headers.get("content-length", 0))
|
|
37
|
+
downloaded = 0
|
|
38
|
+
|
|
39
|
+
with open(tmp_path, "wb") as f:
|
|
40
|
+
for chunk in r.iter_bytes(chunk_size=1024 * 256):
|
|
41
|
+
f.write(chunk)
|
|
42
|
+
downloaded += len(chunk)
|
|
43
|
+
if total:
|
|
44
|
+
pct = downloaded / total
|
|
45
|
+
d_gb = downloaded / 1_073_741_824
|
|
46
|
+
t_gb = total / 1_073_741_824
|
|
47
|
+
on_progress(pct, d_gb, t_gb)
|
|
48
|
+
|
|
49
|
+
tmp_path.rename(MODEL_PATH)
|
|
50
|
+
on_done()
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
on_error(str(e))
|
|
54
|
+
|
|
55
|
+
threading.Thread(target=run, daemon=True).start()
|
|
56
|
+
|
|
57
|
+
def ensure_model():
|
|
58
|
+
if model_exists():
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
print(Fore.CYAN + "┌─────────────────────────────────────────────┐")
|
|
62
|
+
print(Fore.CYAN + "│ GitAI - First run: downloading AI model │")
|
|
63
|
+
print(Fore.CYAN + "│ AI Model Setup - one time only │")
|
|
64
|
+
print(Fore.CYAN + "└─────────────────────────────────────────────┘\n")
|
|
65
|
+
|
|
66
|
+
done_flag = {"done": False, "error": None}
|
|
67
|
+
|
|
68
|
+
def on_progress(pct, d_gb, t_gb):
|
|
69
|
+
bar_len = 30
|
|
70
|
+
filled = int(bar_len * pct)
|
|
71
|
+
bar = Fore.YELLOW + "█" * filled + Style.DIM + "░" * (bar_len - filled)
|
|
72
|
+
print(Fore.YELLOW + f"\r [{bar}" + Style.RESET_ALL + Fore.YELLOW + f"] {pct*100:.1f}% {d_gb:.2f}/{t_gb:.2f} GB", end="", flush=True)
|
|
73
|
+
|
|
74
|
+
def on_done():
|
|
75
|
+
done_flag["done"] = True
|
|
76
|
+
|
|
77
|
+
def on_error(e):
|
|
78
|
+
done_flag["error"] = e
|
|
79
|
+
|
|
80
|
+
download_model(on_progress, on_done, on_error)
|
|
81
|
+
|
|
82
|
+
while not done_flag["done"] and not done_flag["error"]:
|
|
83
|
+
time.sleep(0.5)
|
|
84
|
+
|
|
85
|
+
if done_flag["error"]:
|
|
86
|
+
print(Fore.RED + f"\n\n Error downloading model: {done_flag['error']}")
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
print(Fore.GREEN + "\n\n Model downloaded successfully.\n")
|
|
90
|
+
|
|
91
|
+
def get_repo_language(force_ask=False):
|
|
92
|
+
"""
|
|
93
|
+
Checks if the language configuration exists for this repository.
|
|
94
|
+
If it doesn't exist or force_ask is True, it asks the user and saves it.
|
|
95
|
+
"""
|
|
96
|
+
if not force_ask:
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
["git", "config", "--get", "gitai.lang"],
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
check=False,
|
|
102
|
+
encoding="utf-8",
|
|
103
|
+
errors="replace",
|
|
104
|
+
env=get_secure_env()
|
|
105
|
+
)
|
|
106
|
+
stored_lang = result.stdout.strip().lower()
|
|
107
|
+
if stored_lang in ["es", "en"]:
|
|
108
|
+
return stored_lang
|
|
109
|
+
|
|
110
|
+
print(Fore.CYAN + "\n [gitai] Language configuration for this repository:")
|
|
111
|
+
while True:
|
|
112
|
+
choice = input(Fore.CYAN + " [s] Spanish [e] English → ").strip().lower()
|
|
113
|
+
if choice == "s":
|
|
114
|
+
lang = "es"
|
|
115
|
+
break
|
|
116
|
+
elif choice == "e":
|
|
117
|
+
lang = "en"
|
|
118
|
+
break
|
|
119
|
+
else:
|
|
120
|
+
print(Fore.RED + " Invalid option. Please select 's' or 'e'.")
|
|
121
|
+
|
|
122
|
+
subprocess.run(
|
|
123
|
+
["git", "config", "gitai.lang", lang],
|
|
124
|
+
encoding="utf-8",
|
|
125
|
+
check=False,
|
|
126
|
+
env=get_secure_env()
|
|
127
|
+
)
|
|
128
|
+
print(Fore.GREEN + f" Language saved as '{lang}' in the Git configuration of this project.\n")
|
|
129
|
+
return lang
|
gitai/llm_client.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import psutil
|
|
8
|
+
import threading
|
|
9
|
+
from colorama import init, Fore, Style
|
|
10
|
+
|
|
11
|
+
from gitai.config import MODEL_PATH, PORT
|
|
12
|
+
|
|
13
|
+
init(autoreset=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_daemon_process = None
|
|
17
|
+
_inference_done = False
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
_physical_cores = psutil.cpu_count(logical=False) or psutil.cpu_count() or 4
|
|
21
|
+
_logical_cores = psutil.cpu_count(logical=True) or 4
|
|
22
|
+
except ImportError:
|
|
23
|
+
_logical_cores = os.cpu_count() or 4
|
|
24
|
+
_physical_cores = max(1, _logical_cores // 2)
|
|
25
|
+
|
|
26
|
+
_n_threads_optimized = max(1, _physical_cores)
|
|
27
|
+
_n_threads_batch_optimized = max(1, _logical_cores)
|
|
28
|
+
|
|
29
|
+
def clean_git_diff(raw_diff, max_estimated=2500, debug=True):
|
|
30
|
+
"""
|
|
31
|
+
Preprocesses, cleans, and filters the git diff to optimize processing speed on CPU.
|
|
32
|
+
Includes a debug mode to visualize the filtered string and check character economy.
|
|
33
|
+
"""
|
|
34
|
+
initial_char_count = len(raw_diff)
|
|
35
|
+
lines = raw_diff.splitlines()
|
|
36
|
+
clean_lines = []
|
|
37
|
+
|
|
38
|
+
# List of binary or lockfile patterns that shouldn't waste CPU cycles
|
|
39
|
+
ignored_extensions = ('.png', '.jpg', '.jpeg', '.ico', '.pdf', '.lock')
|
|
40
|
+
|
|
41
|
+
for line in lines:
|
|
42
|
+
# 1. Skip native git metadata that provides zero semantic value to the LLM
|
|
43
|
+
if line.startswith("index ") or line.startswith("similarity index") or line.startswith("rename from") or line.startswith("rename to"):
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
# 2. Skip binary file notifications or tracking modifications
|
|
47
|
+
if "Binary files differ" in line or any(ext in line for ext in ignored_extensions):
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
# 3. Skip pure code comments (Python, JS, C++, etc.)
|
|
51
|
+
if re.match(r'^[+-]\s*(#|//|\*|/\*)', line):
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
# 4. Skip lines that contain only structural syntax/punctuation (brackets, commas, semicolons)
|
|
55
|
+
if re.match(r'^[+-]\s*[{{}}();,.\s]*\s*$', line):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# 5. Skip pure white-space adjustments or empty lines inside the diff
|
|
59
|
+
if not line.strip() or line in ("+", "-"):
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
clean_lines.append(line)
|
|
63
|
+
|
|
64
|
+
filtered_diff = "\n".join(clean_lines)
|
|
65
|
+
|
|
66
|
+
# EMERGENCY STRUCTURAL TRUNCATION (If it's still a monster after filtering)
|
|
67
|
+
if len(filtered_diff) > max_estimated:
|
|
68
|
+
critical_lines = [l for l in clean_lines if l.startswith("---") or l.startswith("+++") or l.startswith("@@")]
|
|
69
|
+
if critical_lines:
|
|
70
|
+
filtered_diff = "\n".join(critical_lines[:70]) + "\n... [Diff truncated structural view due to large size] ..."
|
|
71
|
+
|
|
72
|
+
if debug:
|
|
73
|
+
final_char_count = len(filtered_diff)
|
|
74
|
+
saved_chars = initial_char_count - final_char_count
|
|
75
|
+
|
|
76
|
+
print("\n" + Fore.CYAN + "═"*55)
|
|
77
|
+
print(Fore.CYAN + " [GITAI OPTIMIZER] TRAFFIC ANALYSIS")
|
|
78
|
+
print(Fore.CYAN + "═"*55)
|
|
79
|
+
print(f" • Raw Diff Volume: {initial_char_count} chars")
|
|
80
|
+
print(f" • Clean Data Sent: {final_char_count} chars")
|
|
81
|
+
print(" • Efficiency Bonus: " + Fore.GREEN + f"{saved_chars} chars saved")
|
|
82
|
+
print(Fore.CYAN + "═"*55)
|
|
83
|
+
|
|
84
|
+
global _inference_done
|
|
85
|
+
_inference_done = False
|
|
86
|
+
|
|
87
|
+
def progress_bar():
|
|
88
|
+
bar_length = 30
|
|
89
|
+
total_steps = 350
|
|
90
|
+
|
|
91
|
+
for step in range(total_steps + 1):
|
|
92
|
+
if _inference_done:
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
percent = (step / total_steps) * 100
|
|
96
|
+
if percent > 95:
|
|
97
|
+
percent = 95
|
|
98
|
+
|
|
99
|
+
filled_length = int(bar_length * percent // 100)
|
|
100
|
+
bar = Fore.YELLOW + '█' * filled_length + Style.DIM + '-' * (bar_length - filled_length)
|
|
101
|
+
|
|
102
|
+
sys.stdout.write(Fore.YELLOW + f'\r Crunching diff: [{bar}' + Style.RESET_ALL + Fore.YELLOW + f'] {percent:.0f}%')
|
|
103
|
+
sys.stdout.flush()
|
|
104
|
+
time.sleep(0.2)
|
|
105
|
+
|
|
106
|
+
bar_final = '█' * bar_length
|
|
107
|
+
sys.stdout.write(Fore.YELLOW + f'\r Crunching diff: [{bar_final}' + Fore.YELLOW + '] 100%\n\n')
|
|
108
|
+
sys.stdout.flush()
|
|
109
|
+
|
|
110
|
+
loading_thread = threading.Thread(target=progress_bar)
|
|
111
|
+
loading_thread.start()
|
|
112
|
+
|
|
113
|
+
return filtered_diff
|
|
114
|
+
|
|
115
|
+
def start_daemon(lang="en"):
|
|
116
|
+
"""Starts the llama_cpp server as a silent background process using your optimized parameters."""
|
|
117
|
+
global _daemon_process
|
|
118
|
+
try:
|
|
119
|
+
res = httpx.get(f"http://localhost:{PORT}/v1/models")
|
|
120
|
+
if res.status_code == 200:
|
|
121
|
+
print(Fore.CYAN + "GitAI server daemon is already running in the background.")
|
|
122
|
+
return
|
|
123
|
+
except httpx.RequestError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
print(Fore.CYAN + " Loading model into RAM... (Starting work session)")
|
|
127
|
+
|
|
128
|
+
cmd = [
|
|
129
|
+
sys.executable, "-m", "llama_cpp.server",
|
|
130
|
+
"--model", str(MODEL_PATH),
|
|
131
|
+
"--port", str(PORT),
|
|
132
|
+
"--host", "localhost",
|
|
133
|
+
"--n_ctx", "2048",
|
|
134
|
+
"--n_batch", "512",
|
|
135
|
+
"--n_ubatch", "512",
|
|
136
|
+
"--n_threads", str(_n_threads_optimized),
|
|
137
|
+
"--n_threads_batch", str(_n_threads_batch_optimized),
|
|
138
|
+
"--use_mmap", "True",
|
|
139
|
+
"--cache", "True"
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# WINDOWS CRITICAL FLAG: CREATE_NO_WINDOW (0x08000000)
|
|
143
|
+
# This tells Windows: "Run this in the background 100% invisible,
|
|
144
|
+
# without opening extra CMD windows, but keeping the pipx virtual environment intact".
|
|
145
|
+
creation_flags = 0x08000000
|
|
146
|
+
|
|
147
|
+
# Use close_fds=True to completely unbind file descriptors from the current terminal
|
|
148
|
+
_daemon_process=subprocess.Popen(
|
|
149
|
+
cmd,
|
|
150
|
+
stdout=subprocess.DEVNULL,
|
|
151
|
+
stderr=subprocess.DEVNULL,
|
|
152
|
+
creationflags=creation_flags,
|
|
153
|
+
close_fds=True
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
server_ready = False
|
|
157
|
+
for _ in range(60):
|
|
158
|
+
try:
|
|
159
|
+
time.sleep(1)
|
|
160
|
+
if httpx.get(f"http://localhost:{PORT}/v1/models").status_code == 200:
|
|
161
|
+
server_ready = True
|
|
162
|
+
break
|
|
163
|
+
except httpx.RequestError:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if not server_ready:
|
|
167
|
+
print(Fore.RED + " Error: Server daemon took too long to load into RAM.")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# The prompt is forced to load at startup (Warm-up)
|
|
171
|
+
print(Fore.CYAN + " Priming prompt cache and optimizing engine layers...")
|
|
172
|
+
try:
|
|
173
|
+
# Minimum Plain Text Diff Dummy
|
|
174
|
+
dummy_diff = "--- a/init.txt\n+++ b/init.txt\n@@ -0,0 +1 @@\n+init"
|
|
175
|
+
|
|
176
|
+
# The original function is executed in the background.
|
|
177
|
+
# This will take a few seconds to load in here, absorbing all the initial wait.
|
|
178
|
+
generate_commit_message(diff=dummy_diff, initial_commit=False, lang=lang)
|
|
179
|
+
|
|
180
|
+
print(Fore.GREEN + " Work session initialized. GitAI is hot and ready in the background!")
|
|
181
|
+
except Exception:
|
|
182
|
+
# If for some reason the warm-up fails, do not abort the server boot
|
|
183
|
+
print(Fore.GREEN + " Work session initialized. GitAI is running (cache priming skipped).")
|
|
184
|
+
|
|
185
|
+
def stop_daemon():
|
|
186
|
+
"""Finds the background server process and terminates it to free memory."""
|
|
187
|
+
global _daemon_process
|
|
188
|
+
|
|
189
|
+
if _daemon_process:
|
|
190
|
+
try:
|
|
191
|
+
_daemon_process.terminate()
|
|
192
|
+
_daemon_process.wait(timeout=3)
|
|
193
|
+
_daemon_process = None
|
|
194
|
+
return
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
try:
|
|
198
|
+
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
|
199
|
+
if proc.info['cmdline'] and "llama_cpp.server" in " ".join(proc.info['cmdline']):
|
|
200
|
+
proc.terminate()
|
|
201
|
+
print(Fore.GREEN + "Session closed successfully. RAM cleared.")
|
|
202
|
+
return
|
|
203
|
+
print(Fore.CYAN + "No active GitAI session was found running.")
|
|
204
|
+
except ImportError:
|
|
205
|
+
print(Fore.CYAN + "The 'psutil' library is required to terminate the session. Please install it with: pip install psutil")
|
|
206
|
+
|
|
207
|
+
def generate_commit_message(diff, initial_commit=False, lang="en"):
|
|
208
|
+
"""Generates the commit message by communicating via HTTP with the background daemon."""
|
|
209
|
+
|
|
210
|
+
# Adaptive cleanup and pruning
|
|
211
|
+
diff = clean_git_diff(diff, max_estimated=2500, debug=True)
|
|
212
|
+
|
|
213
|
+
if lang == "es":
|
|
214
|
+
user_instruction = "Genera un mensaje de commit corto basándote en el siguiente git diff:"
|
|
215
|
+
else:
|
|
216
|
+
user_instruction = "Generate a short commit message based on the following git diff:"
|
|
217
|
+
|
|
218
|
+
if initial_commit:
|
|
219
|
+
has_readme = "README.md" in diff or "README.md content" in diff
|
|
220
|
+
if lang == "es":
|
|
221
|
+
if has_readme:
|
|
222
|
+
user_instruction = (
|
|
223
|
+
"Este es el COMMIT INICIAL del repositorio. "
|
|
224
|
+
"Analiza el contenido del README.md y los archivos creados para entender el proyecto. "
|
|
225
|
+
"Genera un mensaje corto que describa la creación del proyecto. NO uses la frase 'initial commit'.\n"
|
|
226
|
+
"Ejemplo: feat: estructura inicial del [nombre del proyecto]"
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
user_instruction = (
|
|
230
|
+
"Este es el COMMIT INICIAL del repositorio. "
|
|
231
|
+
"Basándote únicamente en la lista de archivos creados, genera un mensaje corto "
|
|
232
|
+
"que describa qué tipo de proyecto se está iniciando. NO uses la frase 'initial commit'.\n"
|
|
233
|
+
"Ejemplo: chore: estructura base del [nombre del proyecto]"
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
if has_readme:
|
|
237
|
+
user_instruction = (
|
|
238
|
+
"This is the INITIAL COMMIT of the repository. "
|
|
239
|
+
"Analyze the README.md content and the created files to understand the project's purpose. "
|
|
240
|
+
"Generate a short message describing the project creation. Do NOT use 'initial commit'.\n"
|
|
241
|
+
"Example: feat: initial structure for [project name]"
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
user_instruction = (
|
|
245
|
+
"This is the INITIAL COMMIT of the repository. "
|
|
246
|
+
"Based solely on the list of created files, generate a short one-line message "
|
|
247
|
+
"describing what kind of project is being initiated. Do NOT use 'initial commit'.\n"
|
|
248
|
+
"Example: chore: base repository [project name] structure"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if lang == "es":
|
|
252
|
+
prompt = (
|
|
253
|
+
f"<|im_start|>system\n"
|
|
254
|
+
f"Eres un bot experto en Git que escribe EXCLUSIVAMENTE en idioma ESPAÑOL.\n"
|
|
255
|
+
f"Tu tarea es generar un mensaje de commit usando el estándar de Conventional Commits basándote en el diff provisto.\n\n"
|
|
256
|
+
f"<|im_end|>\n"
|
|
257
|
+
f"<|im_start|>user\n"
|
|
258
|
+
f"ORDEN CRÍTICA (REGLAS DE ORO):\n"
|
|
259
|
+
f"Guía de TIPO de commit (Aplica el primero que corresponda de arriba a abajo):\n"
|
|
260
|
+
f"- 'feat': Si añade código nuevo o nuevas funciones. (Ej: feat(auth): agregar login con google)\n"
|
|
261
|
+
f"- 'fix': Si corrige un error lógico, bug, crash o mal funcionamiento. (Ej: fix(api): corregir timeout en peticion)\n"
|
|
262
|
+
f"- 'refactor': Si limpia, optimiza, renombra o mueve código sin cambiar su comportamiento. (Ej: refactor(llm): optimizar bucle de carga)\n"
|
|
263
|
+
f"- 'style': Si modifica diseño visual, interfaz (UI), colores, fuentes, iconos o tamaños. (Ej: style(ui): cambiar color de boton)\n"
|
|
264
|
+
f"- 'docs': Si el cambio ocurre en README.md, archivos .txt, comentarios o docstrings. (Ej: docs(readme): actualizar guia de instalacion)\n"
|
|
265
|
+
f"- 'chore': Si ocurre SOLO en configuración, dependencias o entornos. (Ej: chore: actualizar paquetes en toml)\n"
|
|
266
|
+
f"- 'test': Añadir, corregir o modificar pruebas unitarias o de integración. (Ej: test(db): agregar prueba de conexion)\n"
|
|
267
|
+
f"- 'perf': Cambios de código destinados específicamente a mejorar la velocidad. (Ej: perf(core): reducir uso de memoria ram)\n"
|
|
268
|
+
f"- 'ci': Modificaciones en scripts de integración o despliegue continuo. (Ej: ci(actions): corregir ruta en workflow de github)\n"
|
|
269
|
+
f"- 'build': Cambios que afectan el empaquetado o herramientas de construcción externa. (Ej: build: empaquetar binario para distribucion)\n"
|
|
270
|
+
f"- Si el diff muestra principalmente líneas eliminadas (-), interpreta si es una limpieza de código muerto (refactor) o una quita de configuración (chore).\n\n"
|
|
271
|
+
f"REGLAS DE FORMATO:\n"
|
|
272
|
+
f"- Formato: tipo: descripción O tipo(scope): descripción (Usa un scope corto entre paréntesis solo si el diff identifica un módulo claro).\n"
|
|
273
|
+
f"- El scope va SIEMPRE entre paréntesis y es UNA SOLA palabra corta. NUNCA uses comas ni extensiones de archivos dentro del scope.\n"
|
|
274
|
+
f"- Todo en minúsculas, sin punto final.\n"
|
|
275
|
+
f"- Devuelve SOLO la línea del commit. No agregues texto introductorio, explicaciones, markdown, bloques de código ni comillas.\n"
|
|
276
|
+
f"- Escribe la respuesta 100% en ESPAÑOL (aunque el código o las variables del diff estén en inglés).\n"
|
|
277
|
+
f"- Máximo 3 a 7 palabras en la descripción. Prohibido pasarte de 7 palabras. Sé directo, descriptivo y mantén el total bajo 72 caracteres.\n\n"
|
|
278
|
+
f"INSTRUCCIÓN ESPECÍFICA PARA ESTE CAMBIO:\n"
|
|
279
|
+
f"{user_instruction}\n\n"
|
|
280
|
+
f"[GIT DIFF TO ANALYZE]:\n{diff}\n"
|
|
281
|
+
f"<|im_end|>\n"
|
|
282
|
+
f"<|im_start|>assistant\n"
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
prompt = (
|
|
286
|
+
f"<|im_start|>system\n"
|
|
287
|
+
f"You are an expert Git bot that writes EXCLUSIVELY in ENGLISH.\n"
|
|
288
|
+
f"Your task is to generate a commit message using the Conventional Commits standard based on the provided diff.\n\n"
|
|
289
|
+
f"<|im_end|>\n"
|
|
290
|
+
f"<|im_start|>user\n"
|
|
291
|
+
f"CRITICAL REMINDER (GOLDEN RULES):\n"
|
|
292
|
+
f"Strict guide to determine the commit TYPE (Apply the first one that matches from top to bottom):\n"
|
|
293
|
+
f"- 'feat': If introduces new executable code or features. (e.g., feat(auth): add google login option)\n"
|
|
294
|
+
f"- 'fix': If fixes logical errors, bugs, crashes, or broken behavior. (e.g., fix(api): fix timeout on endpoint request)\n"
|
|
295
|
+
f"- 'refactor': If cleans, optimizes, renames, or moves code without changing behavior. (e.g., refactor(llm): optimize loading loop structure)\n"
|
|
296
|
+
f"- 'style': If modifies visual presentation, UI, colors, fonts, spacing, icons, or sizing. (e.g., style(ui): change primary button color)\n"
|
|
297
|
+
f"- 'docs': If occurs in README.md, .txt files, comments, or docstrings. (e.g., docs(readme): update setup instructions)\n"
|
|
298
|
+
f"- 'chore': If occurs ONLY in configuration files, dependencies, or setups. (e.g., chore: update packages in toml file)\n"
|
|
299
|
+
f"- 'test': Adding, fixing, or modifying unit/integration tests. (e.g., test(db): add database connection unit test)\n"
|
|
300
|
+
f"- 'perf': Code changes specifically aimed at improving speed. (e.g., perf(core): reduce ram memory usage)\n"
|
|
301
|
+
f"- 'ci': Modifications to continuous integration or delivery scripts. (e.g., ci(actions): fix pathway in github workflow file)\n"
|
|
302
|
+
f"- 'build': Changes affecting the packaging system or external build tools. (e.g., build: package binary executable for distribution)\n"
|
|
303
|
+
f"- If the diff shows mainly deleted lines (-), interpret whether it is a cleanup of dead code (refactor) or a removal of configuration (chore).\n\n"
|
|
304
|
+
f"FORMATTING RULES:\n"
|
|
305
|
+
f"- Format: type: description OR type(scope): description (Use a short scope in parentheses only if the diff clearly identifies a specific module).\n"
|
|
306
|
+
f"- The scope goes ALWAYS in parentheses and is ONE single short word. NEVER use commas or file extensions inside the scope.\n"
|
|
307
|
+
f"- All lowercase, no period at the end.\n"
|
|
308
|
+
f"- Output ONLY the commit message line. No introductions, no explanations, no markdown, no code blocks, no quotes.\n\n"
|
|
309
|
+
f"- Output the final message 100% in ENGLISH (even if the code or variables in the diff are in Spanish).\n"
|
|
310
|
+
f"- Maximum 3 to 7 words in the description. Do NOT exceed 7 words. Be direct, descriptive, and keep the total under 72 characters.\n\n"
|
|
311
|
+
f"SPECIFIC INSTRUCTION FOR THIS CHANGE:\n"
|
|
312
|
+
f"{user_instruction}\n\n"
|
|
313
|
+
f"[GIT DIFF TO ANALYZE]:\n{diff}\n"
|
|
314
|
+
f"<|im_end|>\n"
|
|
315
|
+
f"<|im_start|>assistant\n"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
payload = {
|
|
319
|
+
"prompt": prompt,
|
|
320
|
+
"temperature": 0.0,
|
|
321
|
+
"max_tokens": 100,
|
|
322
|
+
"stop": ["<|im_end|>", "\n"],
|
|
323
|
+
"cache_prompt": True
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
start_time = time.perf_counter()
|
|
328
|
+
response = httpx.post(f"http://localhost:{PORT}/v1/completions", json=payload, timeout=120.0)
|
|
329
|
+
response.raise_for_status()
|
|
330
|
+
|
|
331
|
+
global _inference_done
|
|
332
|
+
_inference_done = True
|
|
333
|
+
time.sleep(0.25)
|
|
334
|
+
|
|
335
|
+
elapsed_time = time.perf_counter() - start_time
|
|
336
|
+
|
|
337
|
+
commit_message = response.json()["choices"][0]["text"].strip()
|
|
338
|
+
|
|
339
|
+
# Cleaning external quotes around the final message
|
|
340
|
+
if commit_message.startswith('"') and commit_message.endswith('"'):
|
|
341
|
+
commit_message = commit_message[1:-1].strip()
|
|
342
|
+
if commit_message.startswith("'") and commit_message.endswith("'"):
|
|
343
|
+
commit_message = commit_message[1:-1].strip()
|
|
344
|
+
|
|
345
|
+
print("\n Inference completed on " + Fore.GREEN + f"{elapsed_time:.2f}s")
|
|
346
|
+
|
|
347
|
+
return commit_message
|
|
348
|
+
|
|
349
|
+
except httpx.RequestError as exc:
|
|
350
|
+
# DIAGNOSIS: The actual technical error that HTTPX is experiencing is printed
|
|
351
|
+
print(Fore.RED + f"\n [DEBUG CLIENT] Technical connection error: {exc}")
|
|
352
|
+
raise RuntimeError(Fore.RED + " GitAI daemon is not running. Please start your session by running: gitai start") from exc
|
|
353
|
+
except Exception as e:
|
|
354
|
+
print(Fore.RED + f"\n [DEBUG CLIENT] Another error: {e}")
|
|
355
|
+
raise e
|
gitai/main.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from colorama import init, Fore, Style
|
|
4
|
+
|
|
5
|
+
from gitai.config import model_exists, ensure_model, get_repo_language, get_secure_env
|
|
6
|
+
from gitai.llm_client import start_daemon, stop_daemon, generate_commit_message
|
|
7
|
+
|
|
8
|
+
init(autoreset=True)
|
|
9
|
+
|
|
10
|
+
def is_initial_commit():
|
|
11
|
+
result = subprocess.run(
|
|
12
|
+
["git", "rev-parse", "HEAD"],
|
|
13
|
+
capture_output=True,
|
|
14
|
+
text=True,
|
|
15
|
+
check=False,
|
|
16
|
+
encoding="utf-8",
|
|
17
|
+
errors="replace",
|
|
18
|
+
env=get_secure_env()
|
|
19
|
+
)
|
|
20
|
+
return result.returncode != 0
|
|
21
|
+
|
|
22
|
+
def get_initial_context():
|
|
23
|
+
context = ""
|
|
24
|
+
for name in ["README.md", "readme.md", "README.txt"]:
|
|
25
|
+
try:
|
|
26
|
+
with open(name, "r", encoding="utf-8", errors="replace") as f:
|
|
27
|
+
head_lines = [f.readline() for _ in range(10)]
|
|
28
|
+
content = "".join(head_lines).strip()
|
|
29
|
+
|
|
30
|
+
if content:
|
|
31
|
+
context += f"README.md content:\n{content}\n\n"
|
|
32
|
+
break
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["git", "diff", "--staged", "--name-only"],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
check=False,
|
|
41
|
+
encoding="utf-8",
|
|
42
|
+
errors="replace",
|
|
43
|
+
env=get_secure_env()
|
|
44
|
+
)
|
|
45
|
+
if result.stdout.strip():
|
|
46
|
+
context += f"Files included in this commit:\n{result.stdout.strip()}"
|
|
47
|
+
|
|
48
|
+
return context
|
|
49
|
+
|
|
50
|
+
def get_staged_diff():
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["git", "diff", "--staged"],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
check=False,
|
|
56
|
+
encoding="utf-8",
|
|
57
|
+
errors="replace",
|
|
58
|
+
env=get_secure_env()
|
|
59
|
+
)
|
|
60
|
+
if result.returncode != 0:
|
|
61
|
+
print(Fore.RED + "Error: this directory is not a git repository.")
|
|
62
|
+
print(Fore.CYAN + "Run 'git init' first.")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
return result.stdout.strip()
|
|
65
|
+
|
|
66
|
+
def run_commit(message):
|
|
67
|
+
result = subprocess.run(
|
|
68
|
+
["git", "commit", "-m", message],
|
|
69
|
+
capture_output=True,
|
|
70
|
+
text=True,
|
|
71
|
+
check=False,
|
|
72
|
+
encoding="utf-8",
|
|
73
|
+
errors="replace",
|
|
74
|
+
env=get_secure_env()
|
|
75
|
+
)
|
|
76
|
+
if result.returncode == 0:
|
|
77
|
+
print(Fore.GREEN + "\nCommit done successfully.")
|
|
78
|
+
print(result.stdout.strip())
|
|
79
|
+
else:
|
|
80
|
+
print(Fore.RED + "\nCommit failed.")
|
|
81
|
+
print(result.stderr.strip())
|
|
82
|
+
|
|
83
|
+
def main():
|
|
84
|
+
args = sys.argv[1:]
|
|
85
|
+
|
|
86
|
+
# 1. GLOBAL SETTINGS: gitai init (Physical download only)
|
|
87
|
+
if args and args[0] == "init":
|
|
88
|
+
ensure_model()
|
|
89
|
+
print(Fore.CYAN + "GitAI global setup finished. Run 'gitai start' inside your repository to start working.")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# 2. START LOGICAL DAY: gitai start (Check local language and raise the model)
|
|
93
|
+
if args and args[0] == "start":
|
|
94
|
+
if not model_exists():
|
|
95
|
+
print(Fore.CYAN + "Model file not found. Run 'gitai init' first to download it.")
|
|
96
|
+
return
|
|
97
|
+
# Smart check: if the language is not set in this repo, it asks for it.
|
|
98
|
+
# On subsequent executions of the command it passes by silently.
|
|
99
|
+
lang = get_repo_language(force_ask=False)
|
|
100
|
+
start_daemon(lang=lang)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# 3. CLOSE WORK SESSION: gitai out
|
|
104
|
+
if args and args[0] == "out":
|
|
105
|
+
stop_daemon()
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# 4. DEFAULT GLOBAL FLOW: gitai (Instant Daily Use)
|
|
109
|
+
if not model_exists():
|
|
110
|
+
print(Fore.CYAN + "GitAI is not initialized. Please run: gitai init")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Silently read local Git configuration without asking questions in terminal
|
|
114
|
+
lang = get_repo_language(force_ask=False)
|
|
115
|
+
|
|
116
|
+
initial = is_initial_commit()
|
|
117
|
+
if initial:
|
|
118
|
+
context = get_initial_context()
|
|
119
|
+
else:
|
|
120
|
+
context = get_staged_diff()
|
|
121
|
+
|
|
122
|
+
if not context:
|
|
123
|
+
print(Fore.CYAN + "Nothing staged. Run 'git add' before using gitai.")
|
|
124
|
+
sys.exit(0)
|
|
125
|
+
|
|
126
|
+
print("Analyzing changes...\n")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
message = generate_commit_message(context, initial_commit=initial, lang=lang)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print(f"\n{e}")
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
print(Fore.CYAN + "┌─────────────────────────────────────────────┐")
|
|
135
|
+
print(Fore.CYAN + "│ Suggested commit message: │")
|
|
136
|
+
print(Fore.CYAN + "└─────────────────────────────────────────────┘")
|
|
137
|
+
print(Fore.GREEN + f"\n {message}\n")
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
choice = input(Fore.CYAN + " [c] Confirm [e] Edit [x] Cancel → ").strip().lower()
|
|
141
|
+
|
|
142
|
+
if choice == "c":
|
|
143
|
+
run_commit(message)
|
|
144
|
+
break
|
|
145
|
+
elif choice == "e":
|
|
146
|
+
edited = input(Fore.CYAN + " Enter your message: ").strip()
|
|
147
|
+
if edited:
|
|
148
|
+
run_commit(edited)
|
|
149
|
+
else:
|
|
150
|
+
print(" Empty message. Cancelled.")
|
|
151
|
+
break
|
|
152
|
+
elif choice == "x":
|
|
153
|
+
print(" Cancelled.")
|
|
154
|
+
break
|
|
155
|
+
else:
|
|
156
|
+
print(Fore.RED + " Invalid option. Use c, e or x.")
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitai-local
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AI-powered conventional commit message generator running 100% locally
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/PabloSalinasDev/GitAI-CLI
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: llama-cpp-python[server]>=0.3.0
|
|
17
|
+
Requires-Dist: httpx>=0.27.0
|
|
18
|
+
Requires-Dist: psutil>=6.0.0
|
|
19
|
+
Requires-Dist: colorama>=0.4.6
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# GitAI
|
|
23
|
+
|
|
24
|
+
An offline, privacy-first CLI tool that automatically generates structured Git commit messages using a local LLM via `llama.cpp`. No API keys required, no data leaves your machine.
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="https://pypi.org/project/gitai-cli/">
|
|
28
|
+
<img src="https://img.shields.io/badge/Install-PyPI-blue?style=for-the-badge&logo=pypi" alt="Install from PyPI">
|
|
29
|
+
</a>
|
|
30
|
+
|
|
31
|
+
<a href="https://github.com/PabloSalinasDev/GitAI-CLI/issues">
|
|
32
|
+
<img src="https://img.shields.io/badge/Report-Error-red?style=for-the-badge&logo=github" alt="Report Error">
|
|
33
|
+
</a>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Why GitAI? (Educational Purpose)
|
|
39
|
+
|
|
40
|
+
Starting out in software development requires absorbing dozens of industry best practices simultaneously. Adopting the **Conventional Commits** standard right from the beginning often causes friction and leads to wasted time staring at a blank terminal screen wondering how to phrase a change.
|
|
41
|
+
|
|
42
|
+
GitAI was built to act as an **invisible mentor inside your console**, specifically designed for developers looking to professionalize their daily workflow without losing focus on writing code.
|
|
43
|
+
|
|
44
|
+
The benefit of implementing this CLI is twofold:
|
|
45
|
+
|
|
46
|
+
* **Immediate Impact (Professionalism):** It resolves technical nomenclature in just a few seconds, ensuring a pristine, clean, and standardized Git history. This is ideal for making your portfolio stand out to recruiters reviewing your GitHub repositories.
|
|
47
|
+
* **Passive Learning (Long-term):** It works through imitation and consistency. By interactively auditing how the AI categorizes your syntax changes with precise tags (`feat`, `fix`, `refactor`), a developer's brain naturally absorbs the pattern over time, learning how to structure correct commits organically.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- **100% Local Ingestion**: Uses `llama-cpp-python` to run model inference completely offline via a background HTTP server on `localhost:8089`.
|
|
54
|
+
- **Conventional Commits**: Enforces the standard `type: description` format (e.g., `feat: add user authentication`).
|
|
55
|
+
- **Diff-Aware Context**: Inspects staged changes and file names to provide accurate context.
|
|
56
|
+
- **Initial Commit Detection**: Automatically detects the first commit of a repository and uses the README and file list as context instead of a diff.
|
|
57
|
+
- **Smart Language Memory**: Learns your language preference (English or Spanish) per repository using Git's native configuration system (`gitai.lang`), avoiding repetitive prompts.
|
|
58
|
+
- **Windows & UTF-8 Native**: Fully hardened against character encoding issues (`ñ`, acentos, `¿`) when generating or editing messages in Windows consoles.
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
1. On first use, run `gitai init` to download the AI model (~4.7 GB).
|
|
63
|
+
2. At the start of each work session, run `gitai start` inside your repo. This loads the model into RAM as a background daemon and asks for your language preference if not already set.
|
|
64
|
+
3. After staging changes, run `gitai`. It feeds the diff (or initial commit context) into the model and proposes a commit message.
|
|
65
|
+
4. At the end of your session, run `gitai out` to stop the daemon and free RAM.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### Real-World Benchmarks & Reference Metrics
|
|
70
|
+
|
|
71
|
+

|
|
72
|
+
|
|
73
|
+
The following metrics are **estimates based on empirical testing**. Actual execution times are non-linear and may vary depending on current CPU background load...
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Project structure
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
gitai/
|
|
81
|
+
├── assets/
|
|
82
|
+
├── gitai/
|
|
83
|
+
│ ├── __init__.py
|
|
84
|
+
│ ├── config.py ← model path, download logic, language config
|
|
85
|
+
│ ├── main.py ← CLI logic, user interaction
|
|
86
|
+
│ └── llm_client.py ← daemon management and inference via HTTP
|
|
87
|
+
├── pyproject.toml ← package definition and dependencies
|
|
88
|
+
└── README.md
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Installation (for Python developers)
|
|
92
|
+
|
|
93
|
+
First, install pipx if you don't have it:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install pipx
|
|
97
|
+
python -m pipx ensurepath
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> After running `ensurepath`, restart your terminal.
|
|
101
|
+
|
|
102
|
+
Then install gitai globally:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pipx install --force .
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Setup (one time)
|
|
109
|
+
|
|
110
|
+
Download the AI model (~4.7 GB). This only needs to be done once:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
gitai init
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> The model (`Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf`) is stored in `%LOCALAPPDATA%\gitai\models`.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Usage
|
|
121
|
+
|
|
122
|
+
### Start a work session
|
|
123
|
+
|
|
124
|
+
Run this once per session inside your repository. Loads the model into RAM and sets the language for the repo if not already configured:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
gitai start
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Generate a commit message
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
git add .
|
|
134
|
+
gitai
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### End a work session
|
|
138
|
+
|
|
139
|
+
Stops the background daemon and frees RAM:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
gitai out
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Command reference
|
|
148
|
+
|
|
149
|
+
| Command | Description |
|
|
150
|
+
|---------|-------------|
|
|
151
|
+
| `gitai init` | Downloads the AI model (first time only) |
|
|
152
|
+
| `gitai start` | Loads the model into RAM, sets repo language |
|
|
153
|
+
| `gitai` | Generates and commits staged changes |
|
|
154
|
+
| `gitai out` | Stops the background daemon, frees RAM |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Commit types used
|
|
159
|
+
|
|
160
|
+
| Type | When to use |
|
|
161
|
+
|------|-------------|
|
|
162
|
+
| feat | New feature |
|
|
163
|
+
| fix | Bug fix |
|
|
164
|
+
| docs | Documentation changes |
|
|
165
|
+
| style | Formatting, no logic change |
|
|
166
|
+
| refactor | Code refactor |
|
|
167
|
+
| test | Adding or fixing tests |
|
|
168
|
+
| chore | Build, dependencies, config |
|
|
169
|
+
| perf | Performance improvement |
|
|
170
|
+
| ci | CI/CD changes |
|
|
171
|
+
| build | Build system changes |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Requirements
|
|
176
|
+
|
|
177
|
+
- Python 3.10+
|
|
178
|
+
- Git installed
|
|
179
|
+
- ~4.7 GB disk space for the model
|
|
180
|
+
|
|
181
|
+
## Performance & Efficiency
|
|
182
|
+
|
|
183
|
+
Commit message generation runs **100% locally and entirely on your CPU**. No external APIs, no data leaks, and no heavy CUDA/ROCm GPU dependencies required.
|
|
184
|
+
|
|
185
|
+
Because inference happens directly on your processor, generation times are non-linear and scale based on your hardware architecture (specifically CPU single-core speed and RAM bandwidth), as well as the complexity of the git diff.
|
|
186
|
+
|
|
187
|
+
### Real-World Benchmarks & Reference Metrics (Qwen 2.5 Coder 7B - Q4_K_M)
|
|
188
|
+
|
|
189
|
+
The following metrics are **estimates based on empirical testing**. Actual execution times are non-linear and may vary depending on current CPU background load and system memory availability.
|
|
190
|
+
|
|
191
|
+
Based on extensive stress-testing on standard consumer hardware (e.g., Intel i7 7th Gen with 16GB DDR4 @ 2400MHz), you can expect the following operational ranges.
|
|
192
|
+
|
|
193
|
+
| Hardware Profile | Context Size | Estimated Time * | Commit Accuracy |
|
|
194
|
+
|------------------|--------------|------------------|-----------------|
|
|
195
|
+
| **Modern Desktop / Laptop**<br>*(Ryzen 5/7, Core i5/i7 11th+ Gen, DDR5)* | Small to Large Diffs | **~8 – 20 seconds** | ~98% |
|
|
196
|
+
| **Older / Standard Hardware**<br>*(Legacy Intel i7, DDR4 @ 2400MHz)* | Small to Medium Diffs | **~20 – 40 seconds** | ~98% |
|
|
197
|
+
| **Stress Test / Massive Changes**<br>*(Legacy Hardware + Dense Diffs)* | Large Multi-file Diffs | **~40 – 70 seconds** | ~98% |
|
|
198
|
+
|
|
199
|
+
> **Note on Performance:** Times are heavily bound to RAM clock speed and single-core efficiency. A dense, multi-file diff analyzed on legacy hardware might occasionally touch the upper boundary of the stress test zone. However, GitAI's built-in traffic manager ensures payloads remain strictly bounded to keep local execution controlled and predictable.
|
|
200
|
+
|
|
201
|
+
> **Predictable Execution Cap:** GitAI features a built-in traffic management engine. By enforcing a hard limit on filtered context sizes, the CLI prevents the LLM from entering runaway processing loops. Even during massive codebase refactors, the input payload is strictly constrained to ensure a reliable, bounded local user experience without ever freezing your terminal.
|
|
202
|
+
|
|
203
|
+
### The GitAI Smart Token Optimization
|
|
204
|
+
|
|
205
|
+
Why are these times so consistent? GitAI does not just dump raw data into the LLM. It includes a custom pre-processing pipeline that strips out compiler noise, binary files, white spaces, and structural brackets before sending the payload.
|
|
206
|
+
|
|
207
|
+
* **Linear vs. Deductive Processing:** Testing proved that sending a complete, clean code block up to 2500 characters is significantly faster than truncating it too early. By providing the model with full, clean context, the LLM processes the data linearly instead of wasting CPU cycles trying to "guess" missing code structures. This balance cuts down processing overhead by up to 30 seconds on older machines while boosting commit accuracy to a staggering **98%**.
|
|
208
|
+
|
|
209
|
+
## Dependencies
|
|
210
|
+
|
|
211
|
+
| Package | Purpose |
|
|
212
|
+
|---------|---------|
|
|
213
|
+
| llama-cpp-python | Run the local AI model as an HTTP server |
|
|
214
|
+
| httpx | Download the model and communicate with the daemon |
|
|
215
|
+
| psutil | Stop the background daemon (`gitai out`) |
|
|
216
|
+
| colorama | Handle cross-platform terminal text colorization and visual hierarchy |
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
*Developed by [Pablo Salinas](https://github.com/PabloSalinasDev)* - PyBloSoft © 2026
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
gitai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
gitai/config.py,sha256=Tl03vFBl9HbvQvHjS-lJBEsuchPFBM498r37Togw64M,4612
|
|
3
|
+
gitai/llm_client.py,sha256=VdDdma6SWjbkk0RlEvz3-od4CyodmoRIM758uX8m8rg,18175
|
|
4
|
+
gitai/main.py,sha256=8QipoVNAlgABveBp0E4xlRSOEBGV2kP6MQz-TouDQRk,5273
|
|
5
|
+
gitai_local-1.0.0.dist-info/licenses/LICENSE,sha256=AwipodiJCJlX9l7_sgnBLIXEVgLyxXgvoY6t2cdIhpw,1075
|
|
6
|
+
gitai_local-1.0.0.dist-info/METADATA,sha256=jb527iV8E8kZ1bVlpLzzNiaBZbAF-tL7ew_GmPH-U3g,9840
|
|
7
|
+
gitai_local-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
gitai_local-1.0.0.dist-info/entry_points.txt,sha256=wwu8sLS-3_iFGu1mZHaS_qYb58pjyBfZ50bF0rfV_Yk,42
|
|
9
|
+
gitai_local-1.0.0.dist-info/top_level.txt,sha256=kB6El44QH2QhSgiaNL2Ms5FFXu7DDaweiFlNhiSW0i8,6
|
|
10
|
+
gitai_local-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
Copyright (c) 2026 GitAI
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
13
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
14
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
15
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
16
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
17
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
18
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gitai
|