1+ #!/usr/bin/env python3
2+ from os import system , path , chmod
3+ from subprocess import check_output , STDOUT , CalledProcessError , run
4+ from argparse import ArgumentParser
5+ from sys import exit , argv
6+ from rich .console import Console
7+ from rich .markdown import Markdown
8+ from time import sleep , time
9+ import threading
10+ import asyncio
11+ from google import genai
12+ import os
13+
14+ console = Console ()
15+
16+ def clear_screen ():
17+ system ("clear" )
18+ console .print ("[bold green]~ ManExplainer v1.0.0 ~[/bold green]\n " )
19+
20+ def delayed_print (text , delay = 0.5 ):
21+ print (text )
22+ sleep (delay )
23+
24+ def install_manexplainer ():
25+ """Install manexplainer as a system command with proper line ending handling"""
26+ script_path = path .abspath (__file__ )
27+ install_path = "/usr/local/bin/manexplainer"
28+
29+ clear_screen ()
30+ console .print ("[bold]Instalando ManExplainer...[/bold]\n " )
31+
32+ try :
33+ # Leer el contenido del script actual
34+ with open (script_path , 'r' , encoding = 'utf-8' ) as f :
35+ content = f .read ()
36+
37+ # Asegurar que tenga el shebang correcto y formato de línea Unix
38+ lines = content .replace ('\r \n ' , '\n ' ).split ('\n ' )
39+
40+ # Comprobar si la primera línea es el shebang
41+ if not lines [0 ].startswith ('#!/usr/bin/env python3' ):
42+ lines .insert (0 , '#!/usr/bin/env python3' )
43+
44+ # Crear archivo temporal con el contenido correcto
45+ temp_file = "/tmp/manexplainer_temp"
46+ with open (temp_file , 'w' , encoding = 'utf-8' , newline = '\n ' ) as f :
47+ f .write ('\n ' .join (lines ))
48+
49+ # Instalar usando sudo
50+ print (f"Instalando en: { install_path } " )
51+ commands = [
52+ f"sudo cp { temp_file } { install_path } " ,
53+ f"sudo chmod +x { install_path } "
54+ ]
55+
56+ for cmd in commands :
57+ result = system (cmd )
58+ if result != 0 :
59+ console .print (f"\n [bold red]✗ Error al ejecutar: { cmd } [/bold red]" )
60+ return False
61+
62+ # Verificar instalación
63+ if path .exists (install_path ):
64+ console .print ("\n [bold green]✓ ManExplainer instalado correctamente[/bold green]" )
65+ console .print ("\n Uso: manexplainer --command COMANDO --query PREGUNTA" )
66+ console .print ("\n Ejemplos:" )
67+ console .print (" manexplainer --command 'ls -la' --query 'Qué significan las columnas?'" )
68+ console .print (" manexplainer --command grep --query 'Cómo busco en múltiples archivos?'" )
69+ return True
70+ else :
71+ console .print ("\n [bold red]✗ Error al instalar ManExplainer[/bold red]" )
72+ return False
73+
74+ except Exception as e :
75+ console .print (f"\n [bold red]✗ Error durante la instalación: { str (e )} [/bold red]" )
76+ return False
77+
78+ async def send_request_with_gemini (prompt , max_retries = 3 , timeout_seconds = 30 ):
79+ """Send request to Gemini AI model with timeout and retry logic"""
80+ retries = 0
81+ final_response = ""
82+
83+ # Initialize Gemini client
84+ api_key = "YOUR_GEMINI_API_KEY"
85+ client = genai .Client (api_key = api_key )
86+ model = "gemini-2.0-flash-exp"
87+ config = {"response_modalities" : ["TEXT" ]}
88+
89+ while retries < max_retries :
90+ try :
91+ if retries > 0 :
92+ delayed_print (f"Reintentando solicitud ({ retries } /{ max_retries } )..." )
93+
94+ final_response = ""
95+ request_failed = False
96+ request_done = False
97+
98+ # Create an event for timeout control
99+ timeout_event = threading .Event ()
100+
101+ # Function to check timeout
102+ def check_timeout ():
103+ sleep (timeout_seconds )
104+ if not timeout_event .is_set () and not request_done :
105+ nonlocal request_failed
106+ request_failed = True
107+ delayed_print ("\n Tiempo de espera agotado. Cancelando solicitud..." )
108+
109+ # Start timeout thread
110+ timeout_thread = threading .Thread (target = check_timeout )
111+ timeout_thread .daemon = True
112+ timeout_thread .start ()
113+
114+ try :
115+ async with client .aio .live .connect (model = model , config = config ) as session :
116+ await session .send_client_content (
117+ turns = {"role" : "user" , "parts" : [{"text" : prompt }]}, turn_complete = True
118+ )
119+
120+ # Reset timeout on connection
121+ timeout_event .set ()
122+ timeout_event .clear ()
123+
124+ if final_response == '' :
125+ clear_screen ()
126+
127+ async for response in session .receive ():
128+ if request_failed :
129+ break
130+
131+ if response .text is not None :
132+ # Reset timeout on data received
133+ timeout_event .set ()
134+ timeout_event .clear ()
135+
136+ print (response .text , end = "" , flush = True )
137+ final_response += response .text
138+
139+ request_done = True
140+ timeout_event .set ()
141+
142+ except Exception as e :
143+ delayed_print (f"Error en la solicitud: { str (e )} " )
144+ request_failed = True
145+
146+ # If request completed successfully and we have a response, exit the loop
147+ if request_done and final_response :
148+ return final_response
149+
150+ # If failed due to timeout or error, increment retry counter
151+ if request_failed or not final_response :
152+ retries += 1
153+ if retries >= max_retries :
154+ delayed_print (f"Error después de { max_retries } intentos. No se pudo obtener respuesta." )
155+ exit (1 )
156+ delayed_print ("Preparando nuevo intento en 2 segundos..." )
157+ sleep (2 )
158+
159+ except Exception as e :
160+ delayed_print (f"Error inesperado: { str (e )} " )
161+ retries += 1
162+ if retries >= max_retries :
163+ delayed_print (f"Error después de { max_retries } intentos." )
164+ exit (1 )
165+ sleep (2 )
166+
167+ return final_response
168+
169+ def print_help ():
170+ """Display help information"""
171+ clear_screen ()
172+ console .print ("[bold]ManExplainer[/bold] - Consulta documentación de comandos con IA\n " )
173+ console .print ("Uso:" )
174+ console .print (" manexplainer --command COMANDO --query PREGUNTA" )
175+ console .print ("\n Argumentos:" )
176+ console .print (" --command COMANDO Comando de terminal para obtener su manual o ejecutar" )
177+ console .print (" --query PREGUNTA Pregunta que se le hará a la IA basada en el manual" )
178+ console .print ("\n Ejemplos:" )
179+ console .print (" manexplainer --command 'ls -la' --query 'Qué significan las columnas?'" )
180+ console .print (" manexplainer --command grep --query 'Cómo busco en múltiples archivos?'" )
181+ console .print ("\n Para instalar:" )
182+ console .print (" python3 manexplainer.py install" )
183+
184+ async def main ():
185+ # Check if running with "install" argument
186+ if len (argv ) > 1 and argv [1 ] == "install" :
187+ install_manexplainer ()
188+ exit (0 )
189+
190+ # Check if no arguments were provided
191+ if len (argv ) == 1 :
192+ print_help ()
193+ exit (0 )
194+
195+ start_time = time ()
196+ clear_screen ()
197+ delayed_print ('Leyendo documentación...' )
198+
199+ # Argumentos CLI
200+ parser = ArgumentParser (description = "Consulta documentación de comandos con IA." )
201+ parser .add_argument ("--command" , nargs = 1 , required = True , help = "Comando de terminal para obtener su manual o ejecutar." )
202+ parser .add_argument ("--query" , nargs = 1 , required = True , help = "Pregunta que se le hará a la IA basada en el manual." )
203+
204+ try :
205+ args = parser .parse_args ()
206+ except SystemExit :
207+ print_help ()
208+ exit (1 )
209+
210+ # Validar que --query tenga un valor
211+ if len (args .query ) != 1 :
212+ print ("Error: --query debe tener exactamente un valor." )
213+ exit (1 )
214+
215+ command = '' .join (args .command ).split (' ' )
216+ command_name = command [0 ]
217+ query = args .query [0 ]
218+
219+ # Verificar si el comando tiene argumentos para ejecutarlo en lugar de usar man
220+ if len (command ) > 1 :
221+ delayed_print (f"Ejecutando comando: { ' ' .join (command )} " )
222+ try :
223+ # Ejecutar el comando completo con sus argumentos
224+ cmd_output = check_output (command , stderr = STDOUT )
225+ cmd_text = cmd_output .decode ('utf-8' )
226+
227+ # Armar prompt con la salida del comando + consulta
228+ prompt = f"""Tengo la siguiente salida del comando `{ ' ' .join (command )} `:
229+
230+ { cmd_text }
231+
232+ Mi pregunta es: { query }
233+ Por favor se conciso, muy técnico y resuelve mi duda usando la menor cantidad de tokens posible, se técnico también.
234+ """
235+ except CalledProcessError as e :
236+ print (f"Error ejecutando '{ ' ' .join (command )} ':\n { e .output .decode ('utf-8' )} " )
237+ exit (1 )
238+ else :
239+ # Obtener salida del comando man si solo hay un argumento
240+ delayed_print (f"Obteniendo manual para: { command_name } " )
241+ try :
242+ man_output = check_output (['man' , command_name ], stderr = STDOUT )
243+ man_text = man_output .decode ('utf-8' )
244+
245+ # Armar prompt con la documentación + consulta
246+ prompt = f"""Tengo el siguiente manual de Linux para el comando `{ command_name } `:
247+
248+ { man_text }
249+
250+ Mi pregunta es: { query }
251+ Por favor se conciso, muy técnico y resuelve mi duda usando la menor cantidad de tokens posible, se técnico también.
252+ """
253+ except CalledProcessError as e :
254+ print (f"Error ejecutando 'man { command_name } ':\n { e .output .decode ('utf-8' )} " )
255+ exit (1 )
256+
257+ clear_screen ()
258+ delayed_print ('Analizando tu pregunta...\n \n ' )
259+
260+ # Enviar solicitud con manejo de timeout y reintentos
261+ final_response = await send_request_with_gemini (prompt )
262+
263+ # Mostrar salida final con Markdown y encabezado
264+ clear_screen ()
265+ console .print (Markdown (final_response ))
266+
267+ # Mostrar tiempo de ejecución
268+ elapsed = time () - start_time
269+ console .print (f"\n [bold green]⏱ Tiempo de respuesta:[/bold green] { elapsed :.2f} segundos\n " )
270+
271+ if __name__ == '__main__' :
272+ asyncio .run (main ())
0 commit comments