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.
- package/LICENSE +21 -0
- package/README.md +288 -0
- package/bin/ocerebro.js +98 -0
- package/cerebro/__init__.py +7 -0
- package/cerebro/__main__.py +19 -0
- package/cerebro/cerebro_setup.py +459 -0
- package/hooks/__init__.py +1 -0
- package/hooks/cost_hook.py +45 -0
- package/hooks/coverage_hook.py +39 -0
- package/hooks/error_hook.py +48 -0
- package/hooks/expensive_hook.py +41 -0
- package/hooks/global_logger.py +32 -0
- package/package.json +49 -0
- package/pyproject.toml +77 -0
- package/src/__init__.py +2 -0
- package/src/cli/__init__.py +2 -0
- package/src/cli/dream.py +91 -0
- package/src/cli/gc.py +93 -0
- package/src/cli/main.py +583 -0
- package/src/cli/remember.py +74 -0
- package/src/consolidation/__init__.py +8 -0
- package/src/consolidation/checkpoints.py +96 -0
- package/src/consolidation/dream.py +465 -0
- package/src/consolidation/extractor.py +313 -0
- package/src/consolidation/promoter.py +435 -0
- package/src/consolidation/remember.py +544 -0
- package/src/consolidation/scorer.py +191 -0
- package/src/core/__init__.py +6 -0
- package/src/core/event_schema.py +55 -0
- package/src/core/jsonl_storage.py +238 -0
- package/src/core/paths.py +254 -0
- package/src/core/session_manager.py +76 -0
- package/src/diff/__init__.py +5 -0
- package/src/diff/memory_diff.py +571 -0
- package/src/forgetting/__init__.py +6 -0
- package/src/forgetting/decay.py +86 -0
- package/src/forgetting/gc.py +296 -0
- package/src/forgetting/guard_rails.py +126 -0
- package/src/hooks/__init__.py +11 -0
- package/src/hooks/core_captures.py +170 -0
- package/src/hooks/custom_loader.py +389 -0
- package/src/index/__init__.py +7 -0
- package/src/index/embeddings_db.py +419 -0
- package/src/index/metadata_db.py +230 -0
- package/src/index/queries.py +357 -0
- package/src/mcp/__init__.py +2 -0
- package/src/mcp/server.py +640 -0
- package/src/memdir/__init__.py +19 -0
- package/src/memdir/scanner.py +260 -0
- package/src/official/__init__.py +5 -0
- package/src/official/markdown_storage.py +173 -0
- package/src/official/templates.py +128 -0
- package/src/working/__init__.py +5 -0
- package/src/working/memory_view.py +150 -0
- 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
|