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,150 @@
1
+ """Geração de MEMORY.md a partir das camadas working/official"""
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from .yaml_storage import YAMLStorage
8
+ from ..official.markdown_storage import MarkdownStorage
9
+
10
+
11
+ class MemoryView:
12
+ """
13
+ Gera MEMORY.md como view de official + working.
14
+
15
+ A memória ativa é uma visão consolidada das camadas:
16
+ - Official Global (decisions, preferences, policies)
17
+ - Official do Projeto (decisions, errors, preferences, state)
18
+ - Working atual (sessions, features em progresso)
19
+ """
20
+
21
+ def __init__(self, cerebro_path: Path, official: "MarkdownStorage", working: "YAMLStorage"):
22
+ """
23
+ Inicializa o MemoryView.
24
+
25
+ Args:
26
+ cerebro_path: Diretório base do Cerebro
27
+ official: Instância do MarkdownStorage
28
+ working: Instância do YAMLStorage
29
+ """
30
+ self.cerebro_path = cerebro_path
31
+ self.official = official
32
+ self.working = working
33
+
34
+ def generate(self, project: str) -> str:
35
+ """
36
+ Gera conteúdo do MEMORY.md para um projeto.
37
+
38
+ Args:
39
+ project: Nome do projeto
40
+
41
+ Returns:
42
+ Conteúdo Markdown do MEMORY.md
43
+ """
44
+ sections = ["# Cerebro - Memória Ativa", ""]
45
+
46
+ # Official Global
47
+ sections.append("## Official Global")
48
+ sections.append("")
49
+ global_items = self._list_global()
50
+ if global_items:
51
+ for item in global_items:
52
+ sections.append(f"- [{item['title']}](global/{item['_type']}/{item['_file']})")
53
+ else:
54
+ sections.append("_Nenhuma memória global_")
55
+ sections.append("")
56
+
57
+ # Official do Projeto
58
+ sections.append(f"## Official {project}")
59
+ sections.append("")
60
+ project_items = self._list_project(project)
61
+ if project_items:
62
+ for item in project_items:
63
+ title = item.get('title', item['_file'])
64
+ sections.append(f"- [{title}]({project}/{item['_type']}/{item['_file']})")
65
+ else:
66
+ sections.append("_Nenhuma memória oficial_")
67
+ sections.append("")
68
+
69
+ # Working atual
70
+ sections.append("## Working atual")
71
+ sections.append("")
72
+ working_items = self._list_working(project)
73
+ if working_items:
74
+ for item in working_items:
75
+ todo = item.get("todo", [])
76
+ sections.append(f"- {item.get('id', 'unknown')}: {item.get('status', 'unknown')}")
77
+ if todo:
78
+ todo_str = ', '.join(todo[:3])
79
+ sections.append(f" - TODO: {todo_str}")
80
+ else:
81
+ sections.append("_Nenhuma sessão em progresso_")
82
+ sections.append("")
83
+
84
+ sections.append("---")
85
+ sections.append("Outras memórias disponíveis via Cerebro (decisions, errors, preferences, state).")
86
+
87
+ return "\n".join(sections)
88
+
89
+ def _list_global(self) -> list:
90
+ """
91
+ Lista memórias globais.
92
+
93
+ Returns:
94
+ Lista de itens globais com metadados
95
+ """
96
+ items = []
97
+ for subdir in ["decisions", "preferences", "policies"]:
98
+ for item in self.official.list_official("global", subdir):
99
+ item["_type"] = subdir
100
+ items.append(item)
101
+ return items
102
+
103
+ def _list_project(self, project: str) -> list:
104
+ """
105
+ Lista memórias do projeto.
106
+
107
+ Args:
108
+ project: Nome do projeto
109
+
110
+ Returns:
111
+ Lista de itens do projeto com metadados
112
+ """
113
+ items = []
114
+ for subdir in ["decisions", "errors", "preferences", "state"]:
115
+ for item in self.official.list_official(project, subdir):
116
+ item["_type"] = subdir
117
+ items.append(item)
118
+ return items
119
+
120
+ def _list_working(self, project: str) -> list:
121
+ """
122
+ Lista working do projeto.
123
+
124
+ Args:
125
+ project: Nome do projeto
126
+
127
+ Returns:
128
+ Lista de sessões e features em progresso
129
+ """
130
+ items = []
131
+ for session in self.working.list_sessions(project):
132
+ items.append(session)
133
+ for feature in self.working.list_features(project):
134
+ items.append(feature)
135
+ return items
136
+
137
+ def write_to_file(self, project: str) -> Path:
138
+ """
139
+ Gera e escreve MEMORY.md no arquivo.
140
+
141
+ Args:
142
+ project: Nome do projeto
143
+
144
+ Returns:
145
+ Path do arquivo MEMORY.md criado
146
+ """
147
+ content = self.generate(project)
148
+ memory_file = self.cerebro_path / "MEMORY.md"
149
+ memory_file.write_text(content, encoding="utf-8")
150
+ return memory_file
@@ -0,0 +1,234 @@
1
+ """Armazenamento YAML para camada Working"""
2
+
3
+ import yaml
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ def _sanitize_name(name: str) -> str:
10
+ """
11
+ Remove caracteres inseguros de nomes de arquivo.
12
+
13
+ SECURITY FIX: Previne path traversal e nomes problemáticos
14
+
15
+ Args:
16
+ name: Nome original
17
+
18
+ Returns:
19
+ Nome sanitizado
20
+ """
21
+ sanitized = re.sub(r'[^\w\-.]', '_', name)
22
+ if sanitized != name:
23
+ import sys
24
+ print(f"[CEREBRO] Nome sanitizado: '{name}' → '{sanitized}'", file=sys.stderr)
25
+ return sanitized
26
+
27
+
28
+ class YAMLStorage:
29
+ """
30
+ Armazenamento YAML para camada Working.
31
+
32
+ Armazena sessões e features em formato YAML estruturado e editável.
33
+ Organização:
34
+ - working/{project}/sessions/{session_id}.yaml
35
+ - working/{project}/features/{feature_name}.yaml
36
+ """
37
+
38
+ def __init__(self, base_path: Path):
39
+ """
40
+ Inicializa o armazenamento YAML.
41
+
42
+ Args:
43
+ base_path: Diretório base para a pasta working/
44
+ """
45
+ self.base_path = base_path
46
+ self.base_path.mkdir(parents=True, exist_ok=True)
47
+
48
+ def _ensure_project_dir(self, project: str, subdir: str) -> Path:
49
+ """
50
+ Garante que diretório do projeto existe.
51
+
52
+ Args:
53
+ project: Nome do projeto
54
+ subdir: Subdiretório (sessions ou features)
55
+
56
+ Returns:
57
+ Path do diretório criado
58
+ """
59
+ dir_path = self.base_path / project / subdir
60
+ dir_path.mkdir(parents=True, exist_ok=True)
61
+ return dir_path
62
+
63
+ def write_session(self, project: str, session_id: str, data: Dict[str, Any]) -> None:
64
+ """
65
+ Escreve sessão em YAML.
66
+
67
+ Args:
68
+ project: Nome do projeto
69
+ session_id: ID da sessão
70
+ data: Dados da sessão
71
+ """
72
+ # SECURITY FIX: Sanitiza nomes
73
+ project = _sanitize_name(project)
74
+ session_id = _sanitize_name(session_id)
75
+
76
+ dir_path = self._ensure_project_dir(project, "sessions")
77
+ yaml_path = dir_path / f"{session_id}.yaml"
78
+
79
+ content = {
80
+ "id": session_id,
81
+ "type": "session",
82
+ **data
83
+ }
84
+
85
+ with open(yaml_path, "w", encoding="utf-8") as f:
86
+ yaml.dump(content, f, allow_unicode=True, default_flow_style=False)
87
+
88
+ def read_session(self, project: str, session_id: str) -> Optional[Dict[str, Any]]:
89
+ """
90
+ Lê sessão de YAML.
91
+
92
+ WINDOWS FIX: encoding="utf-8" explícito
93
+
94
+ Args:
95
+ project: Nome do projeto
96
+ session_id: ID da sessão
97
+
98
+ Returns:
99
+ Dados da sessão ou None se não existir
100
+ """
101
+ # SECURITY FIX: Sanitiza nomes
102
+ project = _sanitize_name(project)
103
+ session_id = _sanitize_name(session_id)
104
+
105
+ yaml_path = self.base_path / project / "sessions" / f"{session_id}.yaml"
106
+
107
+ if not yaml_path.exists():
108
+ return None
109
+
110
+ # WINDOWS FIX: encoding="utf-8" explícito
111
+ return yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
112
+
113
+ def list_sessions(self, project: str, limit: int = 200, status_filter: Optional[str] = None) -> List[Dict[str, Any]]:
114
+ """
115
+ Lista todas as sessões de um projeto.
116
+
117
+ PERFORMANCE FIX: Adiciona limit e status_filter para evitar carregar tudo
118
+
119
+ Args:
120
+ project: Nome do projeto
121
+ limit: Limite de sessões (padrão: 200)
122
+ status_filter: Filtrar por status (opcional)
123
+
124
+ Returns:
125
+ Lista de sessões
126
+ """
127
+ # SECURITY FIX: Sanitiza nome do projeto
128
+ project = _sanitize_name(project)
129
+
130
+ dir_path = self.base_path / project / "sessions"
131
+
132
+ if not dir_path.exists():
133
+ return []
134
+
135
+ sessions = []
136
+ # PERFORMANCE: reverse=True para pegar mais recentes primeiro
137
+ for yaml_file in sorted(dir_path.glob("*.yaml"), reverse=True):
138
+ # WINDOWS FIX: encoding="utf-8" explícito
139
+ data = yaml.safe_load(yaml_file.read_text(encoding="utf-8"))
140
+
141
+ if status_filter and data.get("status") != status_filter:
142
+ continue
143
+
144
+ sessions.append(data)
145
+ if len(sessions) >= limit:
146
+ break
147
+
148
+ return sessions
149
+
150
+ def write_feature(self, project: str, feature_name: str, data: Dict[str, Any]) -> None:
151
+ """
152
+ Escreve feature em YAML.
153
+
154
+ Args:
155
+ project: Nome do projeto
156
+ feature_name: Nome da feature
157
+ data: Dados da feature
158
+ """
159
+ # SECURITY FIX: Sanitiza nomes
160
+ project = _sanitize_name(project)
161
+ feature_name = _sanitize_name(feature_name)
162
+
163
+ dir_path = self._ensure_project_dir(project, "features")
164
+ yaml_path = dir_path / f"{feature_name}.yaml"
165
+
166
+ content = {
167
+ "id": feature_name,
168
+ "type": "feature",
169
+ **data
170
+ }
171
+
172
+ with open(yaml_path, "w", encoding="utf-8") as f:
173
+ yaml.dump(content, f, allow_unicode=True, default_flow_style=False)
174
+
175
+ def read_feature(self, project: str, feature_name: str) -> Optional[Dict[str, Any]]:
176
+ """
177
+ Lê feature de YAML.
178
+
179
+ WINDOWS FIX: encoding="utf-8" explícito
180
+
181
+ Args:
182
+ project: Nome do projeto
183
+ feature_name: Nome da feature
184
+
185
+ Returns:
186
+ Dados da feature ou None se não existir
187
+ """
188
+ # SECURITY FIX: Sanitiza nomes
189
+ project = _sanitize_name(project)
190
+ feature_name = _sanitize_name(feature_name)
191
+
192
+ yaml_path = self.base_path / project / "features" / f"{feature_name}.yaml"
193
+
194
+ if not yaml_path.exists():
195
+ return None
196
+
197
+ # WINDOWS FIX: encoding="utf-8" explícito
198
+ return yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
199
+
200
+ def list_features(self, project: str, limit: int = 200, status_filter: Optional[str] = None) -> List[Dict[str, Any]]:
201
+ """
202
+ Lista todas as features de um projeto.
203
+
204
+ PERFORMANCE FIX: Adiciona limit e status_filter
205
+
206
+ Args:
207
+ project: Nome do projeto
208
+ limit: Limite de features (padrão: 200)
209
+ status_filter: Filtrar por status (opcional)
210
+
211
+ Returns:
212
+ Lista de features
213
+ """
214
+ # SECURITY FIX: Sanitiza nome do projeto
215
+ project = _sanitize_name(project)
216
+
217
+ dir_path = self.base_path / project / "features"
218
+
219
+ if not dir_path.exists():
220
+ return []
221
+
222
+ features = []
223
+ for yaml_file in sorted(dir_path.glob("*.yaml"), reverse=True):
224
+ # WINDOWS FIX: encoding="utf-8" explícito
225
+ data = yaml.safe_load(yaml_file.read_text(encoding="utf-8"))
226
+
227
+ if status_filter and data.get("status") != status_filter:
228
+ continue
229
+
230
+ features.append(data)
231
+ if len(features) >= limit:
232
+ break
233
+
234
+ return features