ocerebro 0.4.16 → 0.4.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocerebro",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
4
4
  "description": "OCerebro - Sistema de Memoria para Agentes (Claude Code/MCP)",
5
5
  "main": "bin/ocerebro.js",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ocerebro"
7
- version = "0.4.16"
7
+ version = "0.4.17"
8
8
  description = "OCerebro - Sistema de Memoria para Agentes (Claude Code/MCP)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
package/src/cli/main.py CHANGED
@@ -333,6 +333,20 @@ def _run_init(project_path: Optional[Path] = None):
333
333
  print()
334
334
  setup_claude(auto=True)
335
335
 
336
+ # FIX 1: Verifica e confirma que hook Stop foi registrado
337
+ print("\n" + "=" * 60)
338
+ print("HOOK AUTOMÁTICO DE FIM DE SESSÃO")
339
+ print("=" * 60)
340
+ print("\nO OCerebro registrou um hook que executa automaticamente")
341
+ print("o comando 'ocerebro dream' toda vez que você encerra uma sessão.")
342
+ print("\nIsso garante que:")
343
+ print(" ✅ Suas memórias são extraídas automaticamente")
344
+ print(" ✅ Zero esforço — use o Claude Code normalmente")
345
+ print(" ✅ Próxima sessão já tem todo o contexto")
346
+ print("\nHook registrado em ~/.claude/settings.json:")
347
+ print(' hooks.Stop → "ocerebro dream --since 1 --apply"')
348
+ print("\nPara desativar: edite settings.json e remova o hook 'Stop'")
349
+
336
350
 
337
351
  def _install_semantic_deps():
338
352
  """Instala dependências de busca semântica (sentence-transformers + spacy)"""
package/src/core/paths.py CHANGED
@@ -23,7 +23,7 @@ def sanitize_path(absolute_path: str) -> str:
23
23
 
24
24
  Exemplo:
25
25
  /home/user/projects/ocerebro → -home-user-projects-ocerebro
26
- C:\\Users\\dev\\my-project → C--Users-dev-my-project
26
+ C:\\Users\\dev\\my-project → -c--users-dev-my-project
27
27
 
28
28
  Args:
29
29
  absolute_path: Path absoluto para sanitizar
@@ -31,26 +31,27 @@ def sanitize_path(absolute_path: str) -> str:
31
31
  Returns:
32
32
  String sanitizada para uso como nome de diretório
33
33
  """
34
+ import re
35
+
34
36
  # Normaliza separadores Windows para Unix
35
37
  normalized = absolute_path.replace("\\", "/")
36
38
 
37
- # Substitui separadores de path por '-'
38
- sanitized = re.sub(r'[/\\]', '-', normalized)
39
+ # Normaliza drive letter para lowercase (E:/ → e:/)
40
+ if len(normalized) >= 2 and normalized[1] == ":":
41
+ normalized = normalized[0].lower() + normalized[1:]
42
+
43
+ # Substitui / e : por -
44
+ sanitized = re.sub(r'[/\\:]', '-', normalized)
39
45
 
40
- # Remove caracteres especiais (mantém apenas alfanuméricos, '-', '_')
46
+ # Remove caracteres especiais
41
47
  sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', sanitized)
42
48
 
43
49
  # Remove múltiplos '-' consecutivos
44
50
  sanitized = re.sub(r'-+', '-', sanitized)
45
51
 
46
- # Garante que começa com '-' se o path original era absoluto
47
- if absolute_path.startswith('/') or absolute_path.startswith('\\'):
48
- if not sanitized.startswith('-'):
49
- sanitized = '-' + sanitized
50
- elif len(absolute_path) >= 2 and absolute_path[1] == ':':
51
- # Windows path (ex: C:\)
52
- if not sanitized.startswith('-'):
53
- sanitized = '-' + sanitized
52
+ # Garante início com -
53
+ if not sanitized.startswith('-'):
54
+ sanitized = '-' + sanitized
54
55
 
55
56
  return sanitized
56
57
 
package/src/mcp/server.py CHANGED
@@ -439,12 +439,38 @@ class CerebroMCP:
439
439
  return [TextContent(type="text", text=f"Erro: {str(e)}")]
440
440
 
441
441
  def _memory(self, args: Dict[str, Any]) -> str:
442
- """Gera memória ativa"""
442
+ """Gera memória ativa e escreve no diretório nativo do Claude Code para auto-load."""
443
443
  project = args.get("project")
444
444
  if not project:
445
445
  return "Erro: project é obrigatório"
446
446
 
447
- return self.memory_view.generate(project)
447
+ content = self.memory_view.generate(project)
448
+
449
+ # FIX 4: Escreve MEMORY.md no diretório nativo do Claude Code
450
+ # Assim o Claude Code carrega automaticamente na próxima sessão
451
+ try:
452
+ from src.core.paths import get_auto_mem_path, get_memory_index
453
+ auto_mem_dir = get_auto_mem_path()
454
+ auto_mem_dir.mkdir(parents=True, exist_ok=True)
455
+ index_path = get_memory_index(auto_mem_dir)
456
+
457
+ # Gera conteúdo compatível com o formato que Claude Code espera
458
+ # Formato: # <title>\n\n- [type] filename (date): description
459
+ claude_format_lines = ["# OCerebro - Memória Ativa", ""]
460
+ claude_format_lines.append(f"## {project}")
461
+ claude_format_lines.append("")
462
+
463
+ # Parse do conteúdo gerado para extrair itens
464
+ for line in content.splitlines():
465
+ if line.startswith("- ["):
466
+ claude_format_lines.append(line)
467
+
468
+ claude_content = "\n".join(claude_format_lines)
469
+ index_path.write_text(claude_content, encoding="utf-8")
470
+ except Exception:
471
+ pass # Falha silenciosa - não bloqueia o retorno
472
+
473
+ return content
448
474
 
449
475
  def _search(self, args: Dict[str, Any]) -> str:
450
476
  """Busca memórias"""
@@ -712,11 +738,11 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
712
738
  """
713
739
 
714
740
  def _capture_memory(self, args: Dict[str, Any]) -> str:
715
- """Salva uma memória no diretório nativo do Claude Code."""
741
+ """Salva uma memória no diretório nativo do Claude Code e no OCerebro (dual-write)."""
716
742
  import re
717
743
  import yaml
718
744
  from datetime import datetime
719
- from src.core.paths import get_memory_index
745
+ from src.core.paths import get_memory_index, get_auto_mem_path
720
746
 
721
747
  content = args.get("memory_content", "")
722
748
  if not content:
@@ -727,11 +753,6 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
727
753
  return "Erro: frontmatter 'name' é obrigatório no memory_content"
728
754
 
729
755
  mem_name = name_match.group(1).strip().lower().replace(' ', '-')
730
- mem_dir = get_auto_mem_path()
731
- mem_dir.mkdir(parents=True, exist_ok=True)
732
-
733
- file_path = mem_dir / f"{mem_name}.md"
734
- file_path.write_text(content, encoding="utf-8")
735
756
 
736
757
  # Parse frontmatter uma única vez com yaml.safe_load
737
758
  frontmatter_match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
@@ -767,18 +788,52 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
767
788
  ts = datetime.now().strftime("%Y-%m-%d")
768
789
  entry = f"- [{m_type}] {mem_name}.md ({ts}): {desc}\n"
769
790
 
770
- index_path = get_memory_index(mem_dir)
771
- if index_path.exists():
772
- existing = index_path.read_text(encoding="utf-8")
791
+ # =========================================================================
792
+ # DUAL-WRITE: Salva em ambos os diretórios
793
+ # =========================================================================
794
+
795
+ # 1. Diretório nativo do Claude Code (~/.claude/projects/<slug>/memory/)
796
+ # → Claude Code carrega automaticamente na próxima sessão
797
+ claude_mem_dir = get_auto_mem_path()
798
+ claude_mem_dir.mkdir(parents=True, exist_ok=True)
799
+ claude_file_path = claude_mem_dir / f"{mem_name}.md"
800
+ claude_file_path.write_text(content, encoding="utf-8")
801
+
802
+ # Atualiza MEMORY.md nativo
803
+ claude_index_path = get_memory_index(claude_mem_dir)
804
+ if claude_index_path.exists():
805
+ existing = claude_index_path.read_text(encoding="utf-8")
773
806
  if mem_name not in existing:
774
- with open(index_path, "a", encoding="utf-8") as f:
807
+ with open(claude_index_path, "a", encoding="utf-8") as f:
775
808
  f.write(entry)
776
809
  else:
777
- index_path.write_text(f"# Memórias do Projeto\n\n{entry}", encoding="utf-8")
810
+ claude_index_path.write_text(f"# Memórias do Projeto\n\n{entry}", encoding="utf-8")
811
+
812
+ # 2. Diretório OCerebro (.ocerebro/official/<subdir>/)
813
+ # → OCerebro indexa e busca via cerebro_memory/cerebro_search
814
+ # Mapeamento: Claude Code type → OCerebro subdir
815
+ type_to_subdir = {
816
+ "user": "decisions", # user → decisions (global)
817
+ "feedback": "preferences", # feedback → preferences
818
+ "project": "decisions", # project → decisions
819
+ "reference": "state", # reference → state
820
+ }
821
+ subdir = type_to_subdir.get(m_type, "decisions") # default: decisions
822
+
823
+ # Para tipo "user", salva em global/; para outros, usa project do frontmatter
824
+ if m_type == "user":
825
+ cerebro_project = "global"
826
+ else:
827
+ cerebro_project = project if project != "unknown" else "default"
828
+
829
+ cerebro_dir = self.cerebro_path / "official" / cerebro_project / subdir
830
+ cerebro_dir.mkdir(parents=True, exist_ok=True)
831
+ cerebro_file_path = cerebro_dir / f"{mem_name}.md"
832
+ cerebro_file_path.write_text(content, encoding="utf-8")
778
833
 
834
+ # =========================================================================
779
835
  # Registrar entidades no grafo (frontmatter + conteúdo)
780
- # ORDEM IMPORTANTE: content primeiro, frontmatter depois
781
- # extract_from_content() deleta apenas entidades 'content', preservando 'frontmatter'
836
+ # =========================================================================
782
837
  if self.entities_db and frontmatter_match:
783
838
  try:
784
839
  # 1. Extrai entidades do conteúdo (spaCy NER)
@@ -791,7 +846,7 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
791
846
  self.entities_db.extract_from_frontmatter(
792
847
  memory_id=mem_name,
793
848
  frontmatter=frontmatter,
794
- project=project
849
+ project=cerebro_project
795
850
  )
796
851
  except Exception:
797
852
  pass # Falha silenciosa se frontmatter inválido
@@ -802,17 +857,17 @@ Uma chamada por memória. O sistema salva e indexa automaticamente.
802
857
  self.metadata_db.insert({
803
858
  "id": mem_name,
804
859
  "type": m_type,
805
- "project": project,
860
+ "project": cerebro_project,
806
861
  "title": frontmatter.get("title", mem_name) if frontmatter else mem_name,
807
862
  "content": body_content,
808
863
  "tags": tags_str,
809
864
  "created_at": datetime.now().isoformat(),
810
865
  "updated_at": datetime.now().isoformat(),
811
866
  "layer": "auto",
812
- "path": str(file_path),
867
+ "path": str(cerebro_file_path),
813
868
  })
814
869
 
815
- return f"✅ Memória '{mem_name}' salva em {file_path}"
870
+ return f"✅ Memória '{mem_name}' salva (dual-write: Claude + OCerebro)"
816
871
 
817
872
  def _remember(self, args: Dict[str, Any]) -> str:
818
873
  """Revisão e promoção de memórias"""
@@ -136,7 +136,7 @@ class MemoryView:
136
136
 
137
137
  def write_to_file(self, project: str) -> Path:
138
138
  """
139
- Gera e escreve MEMORY.md no arquivo.
139
+ Gera e escreve MEMORY.md no arquivo (dual-write: OCerebro + Claude Code nativo).
140
140
 
141
141
  Args:
142
142
  project: Nome do projeto
@@ -145,6 +145,30 @@ class MemoryView:
145
145
  Path do arquivo MEMORY.md criado
146
146
  """
147
147
  content = self.generate(project)
148
+
149
+ # 1. Escreve em .ocerebro/MEMORY.md
148
150
  memory_file = self.cerebro_path / "MEMORY.md"
149
151
  memory_file.write_text(content, encoding="utf-8")
152
+
153
+ # 2. Escreve em ~/.claude/projects/<slug>/memory/MEMORY.md (Claude Code nativo)
154
+ try:
155
+ from src.core.paths import get_auto_mem_path, get_memory_index
156
+ auto_mem_dir = get_auto_mem_path()
157
+ auto_mem_dir.mkdir(parents=True, exist_ok=True)
158
+ index_path = get_memory_index(auto_mem_dir)
159
+
160
+ # Gera conteúdo compatível com formato Claude Code
161
+ claude_format_lines = ["# OCerebro - Memória Ativa", ""]
162
+ claude_format_lines.append(f"## {project}")
163
+ claude_format_lines.append("")
164
+
165
+ for line in content.splitlines():
166
+ if line.startswith("- ["):
167
+ claude_format_lines.append(line)
168
+
169
+ claude_content = "\n".join(claude_format_lines)
170
+ index_path.write_text(claude_content, encoding="utf-8")
171
+ except Exception:
172
+ pass # Falha silenciosa
173
+
150
174
  return memory_file