gemini-cli-pro 0.0.6-snapshot → 0.0.8-snapshot
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.
- package/README.md +1 -1
- package/gemini_tool.py +193 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,5 +52,5 @@ Comandos do chat:
|
|
|
52
52
|
|
|
53
53
|
O arquivo `~/.cache/gemini-history-chats/config.json` controla os MCPs (`local_rag`, `github`, `memory`).
|
|
54
54
|
Se algum MCP nao estiver disponivel, o CLI continua funcionando no modo API.
|
|
55
|
-
O MCP local de RAG usa `
|
|
55
|
+
O MCP local de RAG usa `rag-codebase` via comando `mcp-rag-server` quando estiver instalado.
|
|
56
56
|
Em toda nova sessao, os MCPs sao recarregados automaticamente no startup.
|
package/gemini_tool.py
CHANGED
|
@@ -15,14 +15,37 @@ from typing import Any, Callable
|
|
|
15
15
|
|
|
16
16
|
import google.generativeai as genai
|
|
17
17
|
|
|
18
|
+
try:
|
|
19
|
+
import readline
|
|
20
|
+
except ImportError: # pragma: no cover
|
|
21
|
+
readline = None # type: ignore[assignment]
|
|
22
|
+
|
|
18
23
|
CACHE_DIR = Path.home() / ".cache" / "gemini-history-chats"
|
|
19
24
|
CONFIG_PATH = CACHE_DIR / "config.json"
|
|
20
25
|
MEMORY_PATH = CACHE_DIR / "memory_store.json"
|
|
21
26
|
|
|
22
27
|
MODEL_FLASH = "gemini-2.5-flash"
|
|
23
28
|
MODEL_PRO = "gemini-2.5-pro"
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
RAG_SERVER_COMMAND = "mcp-rag-server"
|
|
30
|
+
|
|
31
|
+
ANSI_RESET = "\033[0m"
|
|
32
|
+
ANSI_BOLD = "\033[1m"
|
|
33
|
+
ANSI_DIM = "\033[2m"
|
|
34
|
+
ANSI_CYAN = "\033[96m"
|
|
35
|
+
ANSI_BLUE = "\033[94m"
|
|
36
|
+
ANSI_GREEN = "\033[92m"
|
|
37
|
+
ANSI_YELLOW = "\033[93m"
|
|
38
|
+
ANSI_RED = "\033[91m"
|
|
39
|
+
ANSI_MAGENTA = "\033[95m"
|
|
40
|
+
ANSI_WHITE = "\033[97m"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def colorize(text: str, color: str) -> str:
|
|
44
|
+
return f"{color}{text}{ANSI_RESET}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def command_history_path_for_cwd() -> Path:
|
|
48
|
+
return CACHE_DIR / f"{get_project_hash()}.cmd_history"
|
|
26
49
|
|
|
27
50
|
|
|
28
51
|
def ensure_cache_layout() -> None:
|
|
@@ -34,9 +57,9 @@ def ensure_cache_layout() -> None:
|
|
|
34
57
|
"mcps": {
|
|
35
58
|
"local_rag": {
|
|
36
59
|
"enabled": True,
|
|
37
|
-
"transport": "
|
|
38
|
-
"
|
|
39
|
-
"description": "Local RAG via
|
|
60
|
+
"transport": "mcp-stdio",
|
|
61
|
+
"server_command": RAG_SERVER_COMMAND,
|
|
62
|
+
"description": "Local RAG via servidor MCP rag-codebase",
|
|
40
63
|
},
|
|
41
64
|
"github": {
|
|
42
65
|
"enabled": True,
|
|
@@ -85,7 +108,7 @@ def merge_dict(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]
|
|
|
85
108
|
def require_api_key() -> str:
|
|
86
109
|
api_key = os.getenv("GOOGLE_API_KEY")
|
|
87
110
|
if not api_key:
|
|
88
|
-
print("Erro: defina GOOGLE_API_KEY antes de iniciar.", file=sys.stderr)
|
|
111
|
+
print(colorize("Erro: defina GOOGLE_API_KEY antes de iniciar.", ANSI_RED), file=sys.stderr)
|
|
89
112
|
sys.exit(1)
|
|
90
113
|
return api_key
|
|
91
114
|
|
|
@@ -200,67 +223,103 @@ def emit_plin() -> None:
|
|
|
200
223
|
print("\a", end="", flush=True)
|
|
201
224
|
|
|
202
225
|
|
|
203
|
-
def
|
|
204
|
-
candidates: list[str] = [
|
|
226
|
+
def _config_rag_server_candidates(config: dict[str, Any]) -> list[str]:
|
|
227
|
+
candidates: list[str] = [RAG_SERVER_COMMAND]
|
|
205
228
|
mcps = config.get("mcps", {})
|
|
206
229
|
if isinstance(mcps, dict):
|
|
207
230
|
local_rag = mcps.get("local_rag", {})
|
|
208
231
|
if isinstance(local_rag, dict):
|
|
209
|
-
configured = local_rag.get("
|
|
232
|
+
configured = local_rag.get("server_command")
|
|
210
233
|
if isinstance(configured, str) and configured.strip():
|
|
211
|
-
candidates.
|
|
234
|
+
candidates.insert(0, configured.strip())
|
|
212
235
|
if isinstance(configured, list):
|
|
213
236
|
for item in configured:
|
|
214
237
|
if isinstance(item, str) and item.strip():
|
|
215
|
-
candidates.
|
|
216
|
-
candidates.append(RAG_FALLBACK_COMMAND)
|
|
217
|
-
# remove duplicados preservando ordem
|
|
238
|
+
candidates.insert(0, item.strip())
|
|
218
239
|
return list(dict.fromkeys(candidates))
|
|
219
240
|
|
|
220
241
|
|
|
221
|
-
def
|
|
242
|
+
def resolve_rag_server_command(config: dict[str, Any] | None = None) -> str | None:
|
|
222
243
|
effective_config = config if isinstance(config, dict) else load_config()
|
|
223
|
-
for candidate in
|
|
244
|
+
for candidate in _config_rag_server_candidates(effective_config):
|
|
224
245
|
if shutil.which(candidate):
|
|
225
246
|
return candidate
|
|
226
247
|
return None
|
|
227
248
|
|
|
228
249
|
|
|
229
|
-
def
|
|
230
|
-
return
|
|
250
|
+
def check_rag_server_available(config: dict[str, Any] | None = None) -> bool:
|
|
251
|
+
return resolve_rag_server_command(config) is not None
|
|
231
252
|
|
|
232
253
|
|
|
233
254
|
def rag_sync_current_dir(config: dict[str, Any] | None = None) -> tuple[bool, str]:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return
|
|
255
|
+
if resolve_rag_server_command(config):
|
|
256
|
+
# Com MCP rag-codebase disponível, considera backend RAG pronto.
|
|
257
|
+
return True, "OK"
|
|
258
|
+
return False, "OFF"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _python_for_script(script_path: str) -> str:
|
|
262
|
+
try:
|
|
263
|
+
with open(script_path, "r", encoding="utf-8") as handle:
|
|
264
|
+
first_line = handle.readline().strip()
|
|
265
|
+
if first_line.startswith("#!") and "python" in first_line:
|
|
266
|
+
return first_line[2:].strip()
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
return sys.executable
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def search_local_context_via_rag_server(query: str) -> str:
|
|
273
|
+
server_command = resolve_rag_server_command()
|
|
274
|
+
if not server_command:
|
|
275
|
+
return ""
|
|
276
|
+
|
|
277
|
+
server_path = shutil.which(server_command)
|
|
278
|
+
if not server_path:
|
|
279
|
+
return ""
|
|
237
280
|
|
|
238
|
-
|
|
281
|
+
runner_code = r'''
|
|
282
|
+
import importlib.util
|
|
283
|
+
import pathlib
|
|
284
|
+
import sys
|
|
285
|
+
|
|
286
|
+
server_file = pathlib.Path(sys.argv[1]).expanduser().resolve()
|
|
287
|
+
query = sys.argv[2]
|
|
288
|
+
top_k = int(sys.argv[3])
|
|
289
|
+
mode = sys.argv[4]
|
|
290
|
+
|
|
291
|
+
spec = importlib.util.spec_from_file_location("mcp_rag_server_runtime", server_file)
|
|
292
|
+
if spec is None or spec.loader is None:
|
|
293
|
+
raise RuntimeError("falha ao carregar modulo do mcp-rag-server")
|
|
294
|
+
mod = importlib.util.module_from_spec(spec)
|
|
295
|
+
spec.loader.exec_module(mod)
|
|
296
|
+
|
|
297
|
+
if not hasattr(mod, "semantic_search_code"):
|
|
298
|
+
raise RuntimeError("semantic_search_code nao encontrado no mcp-rag-server")
|
|
299
|
+
|
|
300
|
+
result = mod.semantic_search_code(query=query, top_k=top_k, mode=mode)
|
|
301
|
+
print(result if isinstance(result, str) else str(result))
|
|
302
|
+
'''
|
|
303
|
+
|
|
304
|
+
py_exec = _python_for_script(server_path)
|
|
305
|
+
result = _run_command([py_exec, "-c", runner_code, server_path, query, "6", "single"], timeout=180)
|
|
306
|
+
stdout = (result.stdout or "").strip()
|
|
307
|
+
stderr = (result.stderr or "").strip()
|
|
239
308
|
if result.returncode == 0:
|
|
240
|
-
return
|
|
241
|
-
return
|
|
309
|
+
return stdout or "Nenhum resultado no rag-codebase."
|
|
310
|
+
return f"Erro no rag-codebase (code={result.returncode}): {stderr or stdout or 'sem detalhes'}"
|
|
242
311
|
|
|
243
312
|
|
|
244
313
|
def search_local_context(query: str) -> str:
|
|
245
|
-
"""Busca no contexto local usando
|
|
246
|
-
|
|
247
|
-
if not
|
|
248
|
-
return "
|
|
314
|
+
"""Busca no contexto local usando o servidor MCP rag-codebase."""
|
|
315
|
+
server_command = resolve_rag_server_command()
|
|
316
|
+
if not server_command:
|
|
317
|
+
return ""
|
|
249
318
|
|
|
250
319
|
query = (query or "").strip()
|
|
251
320
|
if not query:
|
|
252
321
|
return "Consulta vazia."
|
|
253
|
-
|
|
254
|
-
result = _run_command([rag_command, "search", query])
|
|
255
|
-
stdout = (result.stdout or "").strip()
|
|
256
|
-
stderr = (result.stderr or "").strip()
|
|
257
|
-
|
|
258
|
-
if result.returncode == 0:
|
|
259
|
-
return stdout or "Nenhum resultado no RAG local."
|
|
260
|
-
return (
|
|
261
|
-
f"Erro no {rag_command} search (code={result.returncode}): "
|
|
262
|
-
f"{stderr or stdout or 'sem detalhes'}"
|
|
263
|
-
)
|
|
322
|
+
return search_local_context_via_rag_server(query)
|
|
264
323
|
|
|
265
324
|
|
|
266
325
|
def github_tool(action: str, repo: str, path: str = "") -> str:
|
|
@@ -356,7 +415,7 @@ def _mcp_enabled(config: dict[str, Any], mcp_name: str, default: bool = True) ->
|
|
|
356
415
|
|
|
357
416
|
def build_tools(config: dict[str, Any]) -> list[Any]:
|
|
358
417
|
tools: list[Any] = []
|
|
359
|
-
if _mcp_enabled(config, "local_rag", default=True) and
|
|
418
|
+
if _mcp_enabled(config, "local_rag", default=True) and check_rag_server_available(config):
|
|
360
419
|
tools.append(as_executable(search_local_context))
|
|
361
420
|
if _mcp_enabled(config, "github", default=True):
|
|
362
421
|
tools.append(as_executable(github_tool))
|
|
@@ -383,14 +442,66 @@ class GeminiToolCLI:
|
|
|
383
442
|
self.tools_active = False
|
|
384
443
|
self.tools_error = ""
|
|
385
444
|
self.rag_command: str | None = None
|
|
445
|
+
self.rag_server_found = False
|
|
386
446
|
self.rag_ok = False
|
|
387
447
|
self.rag_status = "MISSING"
|
|
448
|
+
self.command_history_file = command_history_path_for_cwd()
|
|
449
|
+
self.readline_enabled = False
|
|
388
450
|
|
|
389
451
|
self.reload_mcps(sync_rag=True, announce=False)
|
|
452
|
+
self._init_readline()
|
|
390
453
|
|
|
391
454
|
self.model = self._build_model(self.current_model_name)
|
|
392
455
|
self.chat = self.model.start_chat(history=self.history)
|
|
393
456
|
|
|
457
|
+
def _init_readline(self) -> None:
|
|
458
|
+
if readline is None or not sys.stdin.isatty():
|
|
459
|
+
return
|
|
460
|
+
try:
|
|
461
|
+
readline.parse_and_bind("set editing-mode emacs")
|
|
462
|
+
readline.parse_and_bind('"\\e[A": previous-history')
|
|
463
|
+
readline.parse_and_bind('"\\e[B": next-history')
|
|
464
|
+
readline.parse_and_bind('"\\e[C": forward-char')
|
|
465
|
+
readline.parse_and_bind('"\\e[D": backward-char')
|
|
466
|
+
readline.set_history_length(1000)
|
|
467
|
+
if self.command_history_file.exists():
|
|
468
|
+
readline.read_history_file(str(self.command_history_file))
|
|
469
|
+
self.readline_enabled = True
|
|
470
|
+
except Exception:
|
|
471
|
+
self.readline_enabled = False
|
|
472
|
+
|
|
473
|
+
def _save_command_history(self) -> None:
|
|
474
|
+
if readline is None or not self.readline_enabled:
|
|
475
|
+
return
|
|
476
|
+
try:
|
|
477
|
+
readline.write_history_file(str(self.command_history_file))
|
|
478
|
+
except Exception:
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
def _record_command_history(self, value: str) -> None:
|
|
482
|
+
if readline is None or not self.readline_enabled:
|
|
483
|
+
return
|
|
484
|
+
text = (value or "").strip()
|
|
485
|
+
if not text:
|
|
486
|
+
return
|
|
487
|
+
last_len = readline.get_current_history_length()
|
|
488
|
+
if last_len > 0 and readline.get_history_item(last_len) == text:
|
|
489
|
+
return
|
|
490
|
+
readline.add_history(text)
|
|
491
|
+
self._save_command_history()
|
|
492
|
+
|
|
493
|
+
def _print_info(self, message: str) -> None:
|
|
494
|
+
print(colorize(message, ANSI_BLUE))
|
|
495
|
+
|
|
496
|
+
def _print_success(self, message: str) -> None:
|
|
497
|
+
print(colorize(message, ANSI_GREEN))
|
|
498
|
+
|
|
499
|
+
def _print_warn(self, message: str) -> None:
|
|
500
|
+
print(colorize(message, ANSI_YELLOW))
|
|
501
|
+
|
|
502
|
+
def _print_error(self, message: str) -> None:
|
|
503
|
+
print(colorize(message, ANSI_RED))
|
|
504
|
+
|
|
394
505
|
def _build_model(self, model_name: str) -> genai.GenerativeModel:
|
|
395
506
|
if not self.tools:
|
|
396
507
|
return genai.GenerativeModel(model_name=model_name)
|
|
@@ -408,7 +519,20 @@ class GeminiToolCLI:
|
|
|
408
519
|
return "FLASH"
|
|
409
520
|
|
|
410
521
|
def _prompt(self) -> str:
|
|
411
|
-
|
|
522
|
+
model_tag = colorize(self._prompt_model_tag(), ANSI_MAGENTA)
|
|
523
|
+
rag_tag = colorize(self.rag_status, ANSI_GREEN if self.rag_status == "OK" else ANSI_YELLOW)
|
|
524
|
+
arrow = f"{ANSI_CYAN}>> {ANSI_WHITE}"
|
|
525
|
+
return f"[{model_tag}][RAG:{rag_tag}] {arrow}"
|
|
526
|
+
|
|
527
|
+
def _read_user_input(self) -> str:
|
|
528
|
+
prompt_text = self._prompt()
|
|
529
|
+
if not sys.stdin.isatty():
|
|
530
|
+
return input(prompt_text).strip()
|
|
531
|
+
print(prompt_text, end="", flush=True)
|
|
532
|
+
try:
|
|
533
|
+
return input().strip()
|
|
534
|
+
finally:
|
|
535
|
+
print(ANSI_RESET, end="", flush=True)
|
|
412
536
|
|
|
413
537
|
def _switch_model_if_needed(self, model_name: str) -> None:
|
|
414
538
|
if model_name == self.current_model_name:
|
|
@@ -480,28 +604,36 @@ class GeminiToolCLI:
|
|
|
480
604
|
self.history.append({"role": role, "parts": [text]})
|
|
481
605
|
|
|
482
606
|
def _print_help(self) -> None:
|
|
483
|
-
|
|
607
|
+
self._print_info("Comandos: /auto, /pro, /flash, /sync, /reload-mcps, /status, /help, /exit")
|
|
484
608
|
|
|
485
609
|
def _show_status(self) -> None:
|
|
486
610
|
tools_state = "ON" if self.tools_active else "OFF"
|
|
487
611
|
rag_cmd = self.rag_command or "none"
|
|
488
612
|
print(
|
|
489
|
-
|
|
490
|
-
|
|
613
|
+
colorize(
|
|
614
|
+
f"mode={self.mode} model={self._prompt_model_tag()} rag={self.rag_status} "
|
|
615
|
+
f"rag_cmd={rag_cmd} tools={tools_state} history={history_path_for_cwd()}",
|
|
616
|
+
ANSI_DIM,
|
|
617
|
+
)
|
|
491
618
|
)
|
|
492
619
|
if self.tools_error:
|
|
493
|
-
|
|
620
|
+
self._print_warn(f"tools_error={self.tools_error}")
|
|
494
621
|
|
|
495
622
|
def _resync_rag(self) -> None:
|
|
496
623
|
rag_ok, rag_status = rag_sync_current_dir(self.config)
|
|
497
624
|
self.rag_ok = rag_ok
|
|
498
625
|
self.rag_status = rag_status
|
|
499
|
-
|
|
626
|
+
if self.rag_ok:
|
|
627
|
+
self._print_success(f"RAG sync status: {self.rag_status}")
|
|
628
|
+
else:
|
|
629
|
+
self._print_warn(f"RAG sync status: {self.rag_status}")
|
|
500
630
|
|
|
501
631
|
def reload_mcps(self, sync_rag: bool = True, announce: bool = True) -> None:
|
|
502
632
|
"""Recarrega MCPs/config no inicio da sessao e sob comando manual."""
|
|
503
633
|
self.config = load_config()
|
|
504
|
-
|
|
634
|
+
rag_server_command = resolve_rag_server_command(self.config)
|
|
635
|
+
self.rag_command = rag_server_command
|
|
636
|
+
self.rag_server_found = rag_server_command is not None
|
|
505
637
|
self.tools = build_tools(self.config)
|
|
506
638
|
self.tools_active = bool(self.tools)
|
|
507
639
|
self.tools_error = ""
|
|
@@ -518,35 +650,32 @@ class GeminiToolCLI:
|
|
|
518
650
|
|
|
519
651
|
if announce:
|
|
520
652
|
rag_info = self.rag_command or "indisponivel"
|
|
521
|
-
|
|
653
|
+
self._print_info(
|
|
522
654
|
f"MCPs recarregados. tools={'ON' if self.tools_active else 'OFF'} "
|
|
523
655
|
f"rag={self.rag_status} cmd={rag_info}"
|
|
524
656
|
)
|
|
525
657
|
|
|
526
658
|
def run(self) -> int:
|
|
527
|
-
|
|
528
|
-
print(f"Historico: {history_path_for_cwd()}")
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
print(
|
|
532
|
-
"Aviso: RAG nao esta OK. Use /sync para tentar novamente ",
|
|
533
|
-
"(ou instale own-rag-cli/own-rag).",
|
|
534
|
-
)
|
|
659
|
+
self._print_info(f"{ANSI_BOLD}Gemini Tool iniciado em:{ANSI_RESET} {os.path.abspath(os.getcwd())}")
|
|
660
|
+
print(colorize(f"Historico: {history_path_for_cwd()}", ANSI_DIM))
|
|
661
|
+
if self.rag_server_found:
|
|
662
|
+
self._print_success("Servidor rag-codebase encontrado. Vou utiliza-lo nesta sessao.")
|
|
535
663
|
|
|
536
664
|
self._print_help()
|
|
537
665
|
|
|
538
666
|
while True:
|
|
539
667
|
try:
|
|
540
|
-
raw =
|
|
668
|
+
raw = self._read_user_input()
|
|
541
669
|
except EOFError:
|
|
542
670
|
print()
|
|
543
671
|
break
|
|
544
672
|
except KeyboardInterrupt:
|
|
545
|
-
|
|
673
|
+
self._print_warn("Interrompido.")
|
|
546
674
|
break
|
|
547
675
|
|
|
548
676
|
if not raw:
|
|
549
677
|
continue
|
|
678
|
+
self._record_command_history(raw)
|
|
550
679
|
|
|
551
680
|
if raw in {"/exit", "/quit", "sair"}:
|
|
552
681
|
break
|
|
@@ -564,17 +693,17 @@ class GeminiToolCLI:
|
|
|
564
693
|
continue
|
|
565
694
|
if raw == "/auto":
|
|
566
695
|
self.mode = "AUTO"
|
|
567
|
-
|
|
696
|
+
self._print_info("Modo alterado para AUTO")
|
|
568
697
|
continue
|
|
569
698
|
if raw == "/pro":
|
|
570
699
|
self.mode = "PRO"
|
|
571
700
|
self._switch_model_if_needed(MODEL_PRO)
|
|
572
|
-
|
|
701
|
+
self._print_info("Modo alterado para PRO")
|
|
573
702
|
continue
|
|
574
703
|
if raw == "/flash":
|
|
575
704
|
self.mode = "FLASH"
|
|
576
705
|
self._switch_model_if_needed(MODEL_FLASH)
|
|
577
|
-
|
|
706
|
+
self._print_info("Modo alterado para FLASH")
|
|
578
707
|
continue
|
|
579
708
|
|
|
580
709
|
decision = self.route_for_input(raw)
|
|
@@ -585,15 +714,16 @@ class GeminiToolCLI:
|
|
|
585
714
|
full_response = ""
|
|
586
715
|
try:
|
|
587
716
|
stream = self.chat.send_message(raw, stream=True)
|
|
717
|
+
print(ANSI_CYAN, end="")
|
|
588
718
|
for chunk in stream:
|
|
589
719
|
piece = self._extract_text(chunk)
|
|
590
720
|
if piece:
|
|
591
721
|
full_response += piece
|
|
592
722
|
print(piece, end="", flush=True)
|
|
593
|
-
print()
|
|
723
|
+
print(ANSI_RESET)
|
|
594
724
|
emit_plin()
|
|
595
725
|
except Exception as exc:
|
|
596
|
-
|
|
726
|
+
self._print_error(f"Erro ao gerar resposta: {exc}")
|
|
597
727
|
self.history.pop()
|
|
598
728
|
emit_plin()
|
|
599
729
|
continue
|
|
@@ -602,6 +732,7 @@ class GeminiToolCLI:
|
|
|
602
732
|
save_history(self.history)
|
|
603
733
|
|
|
604
734
|
save_history(self.history)
|
|
735
|
+
self._save_command_history()
|
|
605
736
|
return 0
|
|
606
737
|
|
|
607
738
|
|