ocerebro 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -0
  3. package/bin/ocerebro.js +98 -0
  4. package/cerebro/__init__.py +7 -0
  5. package/cerebro/__main__.py +19 -0
  6. package/cerebro/cerebro_setup.py +459 -0
  7. package/hooks/__init__.py +1 -0
  8. package/hooks/cost_hook.py +45 -0
  9. package/hooks/coverage_hook.py +39 -0
  10. package/hooks/error_hook.py +48 -0
  11. package/hooks/expensive_hook.py +41 -0
  12. package/hooks/global_logger.py +32 -0
  13. package/package.json +49 -0
  14. package/pyproject.toml +77 -0
  15. package/src/__init__.py +2 -0
  16. package/src/cli/__init__.py +2 -0
  17. package/src/cli/dream.py +91 -0
  18. package/src/cli/gc.py +93 -0
  19. package/src/cli/main.py +583 -0
  20. package/src/cli/remember.py +74 -0
  21. package/src/consolidation/__init__.py +8 -0
  22. package/src/consolidation/checkpoints.py +96 -0
  23. package/src/consolidation/dream.py +465 -0
  24. package/src/consolidation/extractor.py +313 -0
  25. package/src/consolidation/promoter.py +435 -0
  26. package/src/consolidation/remember.py +544 -0
  27. package/src/consolidation/scorer.py +191 -0
  28. package/src/core/__init__.py +6 -0
  29. package/src/core/event_schema.py +55 -0
  30. package/src/core/jsonl_storage.py +238 -0
  31. package/src/core/paths.py +254 -0
  32. package/src/core/session_manager.py +76 -0
  33. package/src/diff/__init__.py +5 -0
  34. package/src/diff/memory_diff.py +571 -0
  35. package/src/forgetting/__init__.py +6 -0
  36. package/src/forgetting/decay.py +86 -0
  37. package/src/forgetting/gc.py +296 -0
  38. package/src/forgetting/guard_rails.py +126 -0
  39. package/src/hooks/__init__.py +11 -0
  40. package/src/hooks/core_captures.py +170 -0
  41. package/src/hooks/custom_loader.py +389 -0
  42. package/src/index/__init__.py +7 -0
  43. package/src/index/embeddings_db.py +419 -0
  44. package/src/index/metadata_db.py +230 -0
  45. package/src/index/queries.py +357 -0
  46. package/src/mcp/__init__.py +2 -0
  47. package/src/mcp/server.py +640 -0
  48. package/src/memdir/__init__.py +19 -0
  49. package/src/memdir/scanner.py +260 -0
  50. package/src/official/__init__.py +5 -0
  51. package/src/official/markdown_storage.py +173 -0
  52. package/src/official/templates.py +128 -0
  53. package/src/working/__init__.py +5 -0
  54. package/src/working/memory_view.py +150 -0
  55. package/src/working/yaml_storage.py +234 -0
@@ -0,0 +1,260 @@
1
+ """Scan de arquivos de memória do Claude Code.
2
+
3
+ Replica exatamente:
4
+ - src/memdir/memoryScan.ts — scanMemoryFiles() + formatMemoryManifest()
5
+ - Lê apenas primeiras 30 linhas (frontmatter)
6
+ - Exclui MEMORY.md do scan
7
+ - Sort por mtime DESC (newest-first)
8
+ - Cap em 200 arquivos
9
+ """
10
+
11
+ import re
12
+ from dataclasses import dataclass
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import List, Literal, Optional
16
+
17
+ from src.core.paths import MAX_MEMORY_FILES, FRONTMATTER_MAX_LINES, MEMORY_INDEX_FILENAME
18
+
19
+
20
+ MemoryType = Literal['user', 'feedback', 'project', 'reference']
21
+
22
+
23
+ @dataclass
24
+ class MemoryHeader:
25
+ """Cabeçalho de arquivo de memória.
26
+
27
+ Replica MemoryHeader do Claude Code (memoryScan.ts).
28
+
29
+ Attributes:
30
+ filename: Path relativo dentro do memory_dir
31
+ file_path: Path absoluto
32
+ mtime_ms: Timestamp em ms (para sort newest-first)
33
+ description: Descrição do frontmatter
34
+ type: Tipo de memória (user/feedback/project/reference)
35
+ name: Nome da memória (do frontmatter)
36
+ """
37
+ filename: str
38
+ file_path: Path
39
+ mtime_ms: float
40
+ description: Optional[str] = None
41
+ type: Optional[MemoryType] = None
42
+ name: Optional[str] = None
43
+
44
+ def to_manifest_line(self) -> str:
45
+ """Formata como linha do manifesto MEMORY.md.
46
+
47
+ Replica formatMemoryManifest() do Claude Code.
48
+
49
+ Formato: "- [type] filename (ISO timestamp): description"
50
+ Exemplo: "- [feedback] feedback_testing.md (2026-03-15T10:00:00Z): não usar mocks"
51
+ """
52
+ # Converte mtime_ms para datetime
53
+ mtime_sec = self.mtime_ms / 1000
54
+ dt = datetime.fromtimestamp(mtime_sec, tz=timezone.utc)
55
+ iso_str = dt.strftime("%Y-%m-%dT%H:%M:%SZ")
56
+
57
+ # Constrói linha
58
+ parts = ["-"]
59
+
60
+ # Adiciona type se existir
61
+ if self.type:
62
+ parts.append(f"[{self.type}]")
63
+
64
+ # Adiciona filename
65
+ parts.append(self.filename)
66
+
67
+ # Adiciona timestamp
68
+ parts.append(f"({iso_str}):")
69
+
70
+ # Adiciona descrição se existir
71
+ if self.description:
72
+ parts.append(self.description)
73
+
74
+ return " ".join(parts)
75
+
76
+
77
+ def parse_frontmatter(text: str) -> dict:
78
+ """
79
+ Parse frontmatter YAML de arquivo de memória.
80
+
81
+ Frontmatter esperado:
82
+ ```
83
+ ---
84
+ name: nome da memória
85
+ description: uma linha — usada para decidir relevância futura
86
+ type: user | feedback | project | reference
87
+ ---
88
+ ```
89
+
90
+ Args:
91
+ text: Conteúdo do arquivo (pelo menos primeiras 30 linhas)
92
+
93
+ Returns:
94
+ Dict com name, description, type extraídos
95
+ """
96
+ result = {
97
+ "name": None,
98
+ "description": None,
99
+ "type": None
100
+ }
101
+
102
+ # Verifica se tem frontmatter
103
+ if not text.strip().startswith("---"):
104
+ return result
105
+
106
+ # Encontra fim do frontmatter
107
+ lines = text.split("\n")
108
+ if len(lines) < 2:
109
+ return result
110
+
111
+ # Procura --- de fechamento
112
+ end_idx = -1
113
+ for i in range(1, min(len(lines), 15)): # Frontmatter normalmente < 15 linhas
114
+ if lines[i].strip() == "---":
115
+ end_idx = i
116
+ break
117
+
118
+ if end_idx == -1:
119
+ return result
120
+
121
+ # Parse linhas do frontmatter
122
+ for i in range(1, end_idx):
123
+ line = lines[i].strip()
124
+ if ":" in line:
125
+ key, value = line.split(":", 1)
126
+ key = key.strip().lower()
127
+ value = value.strip()
128
+
129
+ if key == "name":
130
+ result["name"] = value
131
+ elif key == "description":
132
+ result["description"] = value
133
+ elif key == "type":
134
+ if value in ['user', 'feedback', 'project', 'reference']:
135
+ result["type"] = value
136
+
137
+ return result
138
+
139
+
140
+ def scan_memory_files(memory_dir: Path) -> List[MemoryHeader]:
141
+ """
142
+ Scan de arquivos de memória.
143
+
144
+ Replica scanMemoryFiles() do Claude Code (memoryScan.ts).
145
+
146
+ - readdir recursivo
147
+ - filtra *.md excluindo MEMORY.md
148
+ - lê só as primeiras 30 linhas (frontmatter apenas)
149
+ - parseia frontmatter: name, description, type
150
+ - sort por mtime DESC (newest-first)
151
+ - cap em MAX_MEMORY_FILES=200
152
+ - retorna [] em caso de erro (nunca levanta exceção)
153
+
154
+ Args:
155
+ memory_dir: Diretório de memória para scan
156
+
157
+ Returns:
158
+ Lista de MemoryHeader ordenada por mtime DESC
159
+ """
160
+ if not memory_dir.exists():
161
+ return []
162
+
163
+ try:
164
+ # Coleta todos os arquivos .md (recursivo)
165
+ md_files = list(memory_dir.rglob("*.md"))
166
+ except Exception:
167
+ return []
168
+
169
+ headers = []
170
+
171
+ for file_path in md_files:
172
+ try:
173
+ # Exclui MEMORY.md — regra explícita no código do Claude Code
174
+ if file_path.name == MEMORY_INDEX_FILENAME:
175
+ continue
176
+
177
+ # Pega mtime em milissegundos
178
+ stat = file_path.stat()
179
+ mtime_ms = stat.st_mtime * 1000
180
+
181
+ # Lê apenas primeiras 30 linhas (FRONTMATTER_MAX_LINES)
182
+ try:
183
+ with open(file_path, "r", encoding="utf-8") as f:
184
+ lines = []
185
+ for i, line in enumerate(f):
186
+ if i >= FRONTMATTER_MAX_LINES:
187
+ break
188
+ lines.append(line)
189
+ content = "".join(lines)
190
+ except Exception:
191
+ content = ""
192
+
193
+ # Parse frontmatter
194
+ frontmatter = parse_frontmatter(content)
195
+
196
+ # Cria header
197
+ rel_path = file_path.relative_to(memory_dir)
198
+ header = MemoryHeader(
199
+ filename=str(rel_path),
200
+ file_path=file_path,
201
+ mtime_ms=mtime_ms,
202
+ description=frontmatter.get("description"),
203
+ type=frontmatter.get("type"),
204
+ name=frontmatter.get("name")
205
+ )
206
+ headers.append(header)
207
+
208
+ except Exception:
209
+ # Silenciosamente ignora arquivos com erro
210
+ continue
211
+
212
+ # Sort por mtime DESC (newest-first)
213
+ headers.sort(key=lambda h: h.mtime_ms, reverse=True)
214
+
215
+ # Cap em MAX_MEMORY_FILES
216
+ return headers[:MAX_MEMORY_FILES]
217
+
218
+
219
+ def format_memory_manifest(memories: List[MemoryHeader]) -> str:
220
+ """
221
+ Formata lista de memórias como manifesto para injeção no prompt.
222
+
223
+ Replica formatMemoryManifest() do Claude Code (memoryScan.ts).
224
+
225
+ Formato:
226
+ ```
227
+ Existing memories:
228
+ - [feedback] feedback_testing.md (2026-03-15T10:00:00Z): não usar mocks
229
+ - [project] project_deadline.md (2026-03-14T08:00:00Z): release até sexta
230
+ ```
231
+
232
+ Args:
233
+ memories: Lista de MemoryHeader
234
+
235
+ Returns:
236
+ String formatada para injeção no prompt
237
+ """
238
+ if not memories:
239
+ return "Existing memories: (none)"
240
+
241
+ lines = ["Existing memories:"]
242
+
243
+ for mem in memories:
244
+ lines.append(mem.to_manifest_line())
245
+
246
+ return "\n".join(lines)
247
+
248
+
249
+ def get_existing_memories_summary(memory_dir: Path) -> str:
250
+ """
251
+ Scan + format em uma única função utilitária.
252
+
253
+ Args:
254
+ memory_dir: Diretório de memória para scan
255
+
256
+ Returns:
257
+ Resumo formatado para injeção no prompt
258
+ """
259
+ memories = scan_memory_files(memory_dir)
260
+ return format_memory_manifest(memories)
@@ -0,0 +1,5 @@
1
+ """Camada Official do Cerebro: Markdown durável, versionável"""
2
+ from .markdown_storage import MarkdownStorage
3
+ from .templates import ErrorTemplate, DecisionTemplate
4
+
5
+ __all__ = ["MarkdownStorage", "ErrorTemplate", "DecisionTemplate"]
@@ -0,0 +1,173 @@
1
+ """Armazenamento Markdown para camada Official"""
2
+
3
+ import yaml
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Tuple
7
+
8
+
9
+ class MarkdownStorage:
10
+ """
11
+ Armazenamento Markdown para camada Official.
12
+
13
+ Armazena decisões e erros em formato Markdown com frontmatter YAML.
14
+ Organização:
15
+ - official/{project}/decisions/{name}.md
16
+ - official/{project}/errors/{name}.md
17
+ """
18
+
19
+ def __init__(self, base_path: Path):
20
+ """
21
+ Inicializa o armazenamento Markdown.
22
+
23
+ Args:
24
+ base_path: Diretório base para a pasta official/
25
+ """
26
+ self.base_path = base_path
27
+ self.base_path.mkdir(parents=True, exist_ok=True)
28
+
29
+ def _ensure_project_dir(self, project: str, subdir: str) -> Path:
30
+ """
31
+ Garante que diretório do projeto existe.
32
+
33
+ Args:
34
+ project: Nome do projeto
35
+ subdir: Subdiretório (decisions ou errors)
36
+
37
+ Returns:
38
+ Path do diretório criado
39
+ """
40
+ dir_path = self.base_path / project / subdir
41
+ dir_path.mkdir(parents=True, exist_ok=True)
42
+ return dir_path
43
+
44
+ def _parse_frontmatter(self, content: str) -> Tuple[Dict[str, Any], str]:
45
+ """
46
+ Extrai frontmatter YAML do conteúdo Markdown.
47
+
48
+ Args:
49
+ content: Conteúdo completo do arquivo Markdown
50
+
51
+ Returns:
52
+ Tupla com (frontmatter, corpo)
53
+ """
54
+ match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
55
+
56
+ if not match:
57
+ return {}, content
58
+
59
+ frontmatter = yaml.safe_load(match.group(1))
60
+ body = match.group(2)
61
+
62
+ return frontmatter, body
63
+
64
+ def _format_frontmatter(self, frontmatter: Dict[str, Any]) -> str:
65
+ """
66
+ Formata frontmatter YAML.
67
+
68
+ Args:
69
+ frontmatter: Dicionário com dados do frontmatter
70
+
71
+ Returns:
72
+ String formatada com delimitadores YAML
73
+ """
74
+ return f"---\n{yaml.dump(frontmatter, allow_unicode=True, default_flow_style=False)}---\n"
75
+
76
+ def write_decision(self, project: str, name: str, frontmatter: Dict[str, Any], content: str) -> None:
77
+ """
78
+ Escreve decisão em Markdown.
79
+
80
+ Args:
81
+ project: Nome do projeto
82
+ name: Nome da decisão
83
+ frontmatter: Dados do frontmatter
84
+ content: Corpo da decisão
85
+ """
86
+ dir_path = self._ensure_project_dir(project, "decisions")
87
+ md_path = dir_path / f"{name}.md"
88
+
89
+ frontmatter["type"] = "decision"
90
+ full_content = self._format_frontmatter(frontmatter) + content
91
+
92
+ md_path.write_text(full_content, encoding="utf-8")
93
+
94
+ def read_decision(self, project: str, name: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
95
+ """
96
+ Lê decisão de Markdown.
97
+
98
+ Args:
99
+ project: Nome do projeto
100
+ name: Nome da decisão
101
+
102
+ Returns:
103
+ Tupla com (frontmatter, corpo) ou (None, None) se não existir
104
+ """
105
+ md_path = self.base_path / project / "decisions" / f"{name}.md"
106
+
107
+ if not md_path.exists():
108
+ return None, None
109
+
110
+ content = md_path.read_text(encoding="utf-8")
111
+ return self._parse_frontmatter(content)
112
+
113
+ def write_error(self, project: str, name: str, frontmatter: Dict[str, Any], content: str) -> None:
114
+ """
115
+ Escreve erro em Markdown.
116
+
117
+ Args:
118
+ project: Nome do projeto
119
+ name: Nome do erro
120
+ frontmatter: Dados do frontmatter
121
+ content: Corpo do post-mortem
122
+ """
123
+ dir_path = self._ensure_project_dir(project, "errors")
124
+ md_path = dir_path / f"{name}.md"
125
+
126
+ frontmatter["type"] = "error"
127
+ full_content = self._format_frontmatter(frontmatter) + content
128
+
129
+ md_path.write_text(full_content, encoding="utf-8")
130
+
131
+ def read_error(self, project: str, name: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
132
+ """
133
+ Lê erro de Markdown.
134
+
135
+ Args:
136
+ project: Nome do projeto
137
+ name: Nome do erro
138
+
139
+ Returns:
140
+ Tupla com (frontmatter, corpo) ou (None, None) se não existir
141
+ """
142
+ md_path = self.base_path / project / "errors" / f"{name}.md"
143
+
144
+ if not md_path.exists():
145
+ return None, None
146
+
147
+ content = md_path.read_text(encoding="utf-8")
148
+ return self._parse_frontmatter(content)
149
+
150
+ def list_official(self, project: str, subdir: str) -> List[Dict[str, Any]]:
151
+ """
152
+ Lista itens de um subdiretório official.
153
+
154
+ Args:
155
+ project: Nome do projeto
156
+ subdir: Subdiretório (decisions ou errors)
157
+
158
+ Returns:
159
+ Lista de frontmatters com nome do arquivo
160
+ """
161
+ dir_path = self.base_path / project / subdir
162
+
163
+ if not dir_path.exists():
164
+ return []
165
+
166
+ items = []
167
+ for md_file in sorted(dir_path.glob("*.md")):
168
+ frontmatter, _ = self._parse_frontmatter(md_file.read_text(encoding="utf-8"))
169
+ if frontmatter:
170
+ frontmatter["_file"] = md_file.name
171
+ items.append(frontmatter)
172
+
173
+ return items
@@ -0,0 +1,128 @@
1
+ """Templates para camada Official do Cerebro"""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+
6
+ class ErrorTemplate:
7
+ """Template para post-mortem de erro"""
8
+
9
+ @staticmethod
10
+ def frontmatter(
11
+ error_id: str,
12
+ severity: str,
13
+ status: str,
14
+ category: str,
15
+ area: str,
16
+ project: str,
17
+ tags: List[str] = None,
18
+ related_to: List[str] = None,
19
+ similar_to: List[str] = None
20
+ ) -> Dict[str, Any]:
21
+ """Cria frontmatter para erro"""
22
+ return {
23
+ "id": error_id,
24
+ "type": "error",
25
+ "status": status,
26
+ "severity": severity,
27
+ "impact": severity,
28
+ "category": category,
29
+ "area": area,
30
+ "project": project,
31
+ "tags": tags or [],
32
+ "related_to": related_to or [],
33
+ "similar_to": similar_to or []
34
+ }
35
+
36
+ @staticmethod
37
+ def body(
38
+ error_original: str,
39
+ causa_raiz: str,
40
+ solucao_aplicada: str,
41
+ prevencao_futura: Optional[str] = None
42
+ ) -> str:
43
+ """Cria corpo do post-mortem de erro"""
44
+ sections = [
45
+ "# Erro Original",
46
+ "",
47
+ error_original,
48
+ "",
49
+ "# Causa Raiz",
50
+ "",
51
+ causa_raiz,
52
+ "",
53
+ "# Solução Aplicada",
54
+ "",
55
+ solucao_aplicada
56
+ ]
57
+
58
+ if prevencao_futura:
59
+ sections.extend([
60
+ "",
61
+ "# Prevenção Futura",
62
+ "",
63
+ prevencao_futura
64
+ ])
65
+
66
+ return "\n".join(sections)
67
+
68
+
69
+ class DecisionTemplate:
70
+ """Template para decisão arquitetural"""
71
+
72
+ @staticmethod
73
+ def frontmatter(
74
+ decision_id: str,
75
+ title: str,
76
+ status: str,
77
+ date: str,
78
+ project: str,
79
+ tags: List[str] = None,
80
+ related_to: List[str] = None
81
+ ) -> Dict[str, Any]:
82
+ """Cria frontmatter para decisão"""
83
+ return {
84
+ "id": decision_id,
85
+ "type": "decision",
86
+ "title": title,
87
+ "status": status,
88
+ "date": date,
89
+ "project": project,
90
+ "tags": tags or [],
91
+ "related_to": related_to or []
92
+ }
93
+
94
+ @staticmethod
95
+ def body(
96
+ contexto: str,
97
+ decisao: str,
98
+ alternativas: Optional[str] = None,
99
+ consequencias: Optional[str] = None
100
+ ) -> str:
101
+ """Cria corpo da decisão"""
102
+ sections = [
103
+ "# Contexto",
104
+ "",
105
+ contexto,
106
+ "",
107
+ "# Decisão",
108
+ "",
109
+ decisao
110
+ ]
111
+
112
+ if alternativas:
113
+ sections.extend([
114
+ "",
115
+ "# Alternativas Consideradas",
116
+ "",
117
+ alternativas
118
+ ])
119
+
120
+ if consequencias:
121
+ sections.extend([
122
+ "",
123
+ "# Consequências",
124
+ "",
125
+ consequencias
126
+ ])
127
+
128
+ return "\n".join(sections)
@@ -0,0 +1,5 @@
1
+ """Camada Working do Cerebro: YAML estruturado, editável"""
2
+ from .yaml_storage import YAMLStorage
3
+ from .memory_view import MemoryView
4
+
5
+ __all__ = ["YAMLStorage", "MemoryView"]