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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/gemini_tool.py +193 -62
  3. 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 `own-rag-cli` como nativo e cai para `own-rag` se existir.
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
- RAG_NATIVE_COMMAND = "own-rag-cli"
25
- RAG_FALLBACK_COMMAND = "own-rag"
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": "cli",
38
- "command": RAG_NATIVE_COMMAND,
39
- "description": "Local RAG via own-rag-cli (fallback own-rag)",
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 _config_rag_candidates(config: dict[str, Any]) -> list[str]:
204
- candidates: list[str] = [RAG_NATIVE_COMMAND]
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("command")
232
+ configured = local_rag.get("server_command")
210
233
  if isinstance(configured, str) and configured.strip():
211
- candidates.append(configured.strip())
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.append(item.strip())
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 resolve_rag_command(config: dict[str, Any] | None = None) -> str | None:
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 _config_rag_candidates(effective_config):
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 check_own_rag_installed(config: dict[str, Any] | None = None) -> bool:
230
- return resolve_rag_command(config) is not None
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
- rag_command = resolve_rag_command(config)
235
- if not rag_command:
236
- return False, "MISSING"
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
- result = _run_command([rag_command, "sync", "."])
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 True, "OK"
241
- return False, "FAIL"
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 own-rag search."""
246
- rag_command = resolve_rag_command()
247
- if not rag_command:
248
- return "own-rag-cli/own-rag nao encontrado no PATH."
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 check_own_rag_installed(config):
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
- return f"[{self._prompt_model_tag()}][RAG:{self.rag_status}] >> "
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
- print("Comandos: /auto, /pro, /flash, /sync, /reload-mcps, /status, /help, /exit")
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
- f"mode={self.mode} model={self._prompt_model_tag()} rag={self.rag_status} "
490
- f"rag_cmd={rag_cmd} tools={tools_state} history={history_path_for_cwd()}"
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
- print(f"tools_error={self.tools_error}")
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
- print(f"RAG sync status: {self.rag_status}")
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
- self.rag_command = resolve_rag_command(self.config)
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
- print(
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
- print(f"Gemini Tool iniciado em: {os.path.abspath(os.getcwd())}")
528
- print(f"Historico: {history_path_for_cwd()}")
529
-
530
- if not self.rag_ok:
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 = input(self._prompt()).strip()
668
+ raw = self._read_user_input()
541
669
  except EOFError:
542
670
  print()
543
671
  break
544
672
  except KeyboardInterrupt:
545
- print("\nInterrompido.")
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
- print("Modo alterado para AUTO")
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
- print("Modo alterado para PRO")
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
- print("Modo alterado para FLASH")
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
- print(f"Erro ao gerar resposta: {exc}")
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemini-cli-pro",
3
- "version": "0.0.6-snapshot",
3
+ "version": "0.0.8-snapshot",
4
4
  "description": "Gemini CLI em Python com sync de RAG local e roteamento Flash/Pro",
5
5
  "license": "MIT",
6
6
  "bin": {