ocerebro 0.3.2 → 0.4.0

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/src/mcp/server.py CHANGED
@@ -371,6 +371,20 @@ class CerebroMCP:
371
371
  },
372
372
  "required": ["entity"]
373
373
  }
374
+ ),
375
+ Tool(
376
+ name="cerebro_dashboard",
377
+ description="Abre o dashboard visual do OCerebro no browser (localhost:7999) - interface gráfica para explorar memórias, grafo de entidades e timeline",
378
+ inputSchema={
379
+ "type": "object",
380
+ "properties": {
381
+ "port": {
382
+ "type": "integer",
383
+ "description": "Porta do servidor (padrão: 7999)",
384
+ "default": 7999
385
+ }
386
+ }
387
+ }
374
388
  )
375
389
  ]
376
390
 
@@ -410,6 +424,8 @@ class CerebroMCP:
410
424
  result = self._capture_memory(arguments)
411
425
  elif name == "cerebro_graph":
412
426
  result = self._cerebro_graph(arguments)
427
+ elif name == "cerebro_dashboard":
428
+ result = self._cerebro_dashboard(arguments)
413
429
  else:
414
430
  return [TextContent(type="text", text=f"Ferramenta desconhecida: {name}")]
415
431
 
@@ -693,6 +709,7 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
693
709
  def _capture_memory(self, args: Dict[str, Any]) -> str:
694
710
  """Salva uma memória no diretório nativo do Claude Code."""
695
711
  import re
712
+ import yaml
696
713
  from datetime import datetime
697
714
  from src.core.paths import get_memory_index
698
715
 
@@ -711,15 +728,36 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
711
728
  file_path = mem_dir / f"{mem_name}.md"
712
729
  file_path.write_text(content, encoding="utf-8")
713
730
 
714
- desc_match = re.search(r'description:\s*(.*)', content)
715
- type_match = re.search(r'type:\s*(.*)', content)
716
- project_match = re.search(r'project:\s*(.*)', content)
717
- tags_match = re.search(r'tags:\s*(.*)', content)
718
-
719
- desc = desc_match.group(1).strip() if desc_match else "sem descrição"
720
- m_type = type_match.group(1).strip() if type_match else "project"
721
- project = project_match.group(1).strip() if project_match else "unknown"
722
- tags = tags_match.group(1).strip() if tags_match else ""
731
+ # Parse frontmatter uma única vez com yaml.safe_load
732
+ frontmatter_match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
733
+ frontmatter = {}
734
+ body_content = ""
735
+ if frontmatter_match:
736
+ try:
737
+ frontmatter = yaml.safe_load(frontmatter_match.group(1)) or {}
738
+ body_content = frontmatter_match.group(2)
739
+ except Exception:
740
+ pass # Fallback para regex se yaml falhar
741
+
742
+ # Extrai variáveis do frontmatter parseado (fallback para regex se necessário)
743
+ m_type = frontmatter.get('type', '')
744
+ project = frontmatter.get('project', '')
745
+ tags = frontmatter.get('tags', '')
746
+ desc = frontmatter.get('description', '')
747
+
748
+ # Fallback via regex se frontmatter parsing falhou
749
+ if not m_type:
750
+ type_match = re.search(r'type:\s*(.*)', content)
751
+ m_type = type_match.group(1).strip() if type_match else "project"
752
+ if not project:
753
+ project_match = re.search(r'project:\s*(.*)', content)
754
+ project = project_match.group(1).strip() if project_match else "unknown"
755
+ if not tags:
756
+ tags_match = re.search(r'tags:\s*(.*)', content)
757
+ tags = tags_match.group(1).strip() if tags_match else ""
758
+ if not desc:
759
+ desc_match = re.search(r'description:\s*(.*)', content)
760
+ desc = desc_match.group(1).strip() if desc_match else "sem descrição"
723
761
 
724
762
  ts = datetime.now().strftime("%Y-%m-%d")
725
763
  entry = f"- [{m_type}] {mem_name}.md ({ts}): {desc}\n"
@@ -733,30 +771,25 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
733
771
  else:
734
772
  index_path.write_text(f"# Memórias do Projeto\n\n{entry}", encoding="utf-8")
735
773
 
736
- # BUG FIX: Registrar entidades no grafo (frontmatter + conteúdo)
774
+ # Registrar entidades no grafo (frontmatter + conteúdo)
737
775
  # ORDEM IMPORTANTE: content primeiro, frontmatter depois
738
- # extract_from_content() deleta entidades existentes, então frontmatter deve vir após
739
- if self.entities_db:
740
- import yaml
741
- frontmatter_match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
742
- if frontmatter_match:
743
- try:
744
- frontmatter = yaml.safe_load(frontmatter_match.group(1))
745
- body_content = frontmatter_match.group(2)
746
- # 1. Extrai entidades do conteúdo (spaCy NER) - pode deletar existentes
747
- self.entities_db.extract_from_content(
748
- memory_id=mem_name,
749
- content=body_content,
750
- use_spacy=True
751
- )
752
- # 2. Extrai entidades do frontmatter - NÃO são deletadas
753
- self.entities_db.extract_from_frontmatter(
754
- memory_id=mem_name,
755
- frontmatter=frontmatter or {},
756
- project=project
757
- )
758
- except Exception as e:
759
- pass # Falha silenciosa se frontmatter inválido
776
+ # extract_from_content() deleta apenas entidades 'content', preservando 'frontmatter'
777
+ if self.entities_db and frontmatter_match:
778
+ try:
779
+ # 1. Extrai entidades do conteúdo (spaCy NER)
780
+ self.entities_db.extract_from_content(
781
+ memory_id=mem_name,
782
+ content=body_content,
783
+ use_spacy=True
784
+ )
785
+ # 2. Extrai entidades do frontmatter - preservadas
786
+ self.entities_db.extract_from_frontmatter(
787
+ memory_id=mem_name,
788
+ frontmatter=frontmatter,
789
+ project=project
790
+ )
791
+ except Exception:
792
+ pass # Falha silenciosa se frontmatter inválido
760
793
 
761
794
  return f"✅ Memória '{mem_name}' salva em {file_path}"
762
795
 
@@ -862,6 +895,36 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
862
895
 
863
896
  return "\n".join(lines)
864
897
 
898
+ def _cerebro_dashboard(self, args: Dict[str, Any]) -> str:
899
+ """Abre o dashboard visual do OCerebro no browser"""
900
+ try:
901
+ from src.dashboard.server import DashboardServer
902
+
903
+ port = args.get("port", 7999)
904
+
905
+ # Instancia servidor
906
+ dashboard_server = DashboardServer(
907
+ cerebro_path=self.cerebro_path,
908
+ metadata_db=self.metadata_db,
909
+ embeddings_db=self.embeddings_db,
910
+ entities_db=self.entities_db
911
+ )
912
+
913
+ # Inicia se não estiver rodando
914
+ if not dashboard_server.is_running(port):
915
+ started = dashboard_server.start(port)
916
+ if not started:
917
+ return "⚠️ Não foi possível iniciar o servidor do dashboard. Verifique se a porta está disponível."
918
+
919
+ # Abre browser
920
+ dashboard_server.open_browser(port)
921
+
922
+ return f"✅ Dashboard aberto em http://localhost:{port}"
923
+ except ImportError as e:
924
+ return f"Erro: Não foi possível importar o dashboard. Verifique se fastapi e uvicorn estão instalados. ({e})"
925
+ except Exception as e:
926
+ return f"Erro ao abrir dashboard: {e}"
927
+
865
928
 
866
929
  async def main():
867
930
  """Entry point do MCP Server"""