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,296 @@
|
|
|
1
|
+
"""Garbage collection para memórias do Cerebro.
|
|
2
|
+
|
|
3
|
+
Replica a lógica de garbage collection do Claude Code:
|
|
4
|
+
- Filtra por mtime do arquivo (última modificação)
|
|
5
|
+
- Nunca remove memórias de tipo 'user' ou 'feedback'
|
|
6
|
+
- Aplica threshold de dias sem acesso para working sessions
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from src.memdir.scanner import parse_frontmatter
|
|
14
|
+
from src.core.paths import MAX_MEMORY_FILES
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GarbageCollector:
|
|
18
|
+
"""
|
|
19
|
+
Garbage collection para memórias.
|
|
20
|
+
|
|
21
|
+
Identifica memórias candidatas para arquivamento ou remoção
|
|
22
|
+
baseado em policies de forgetting.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, config_path: Path):
|
|
26
|
+
"""
|
|
27
|
+
Inicializa o GarbageCollector.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
config_path: Path para configuração
|
|
31
|
+
"""
|
|
32
|
+
self.config_path = config_path
|
|
33
|
+
|
|
34
|
+
def find_candidates_for_archive(
|
|
35
|
+
self,
|
|
36
|
+
memory_dir: Path,
|
|
37
|
+
days_threshold: int
|
|
38
|
+
) -> List[Dict[str, Any]]:
|
|
39
|
+
"""
|
|
40
|
+
Encontra memórias candidatas para arquivamento.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
memory_dir: Diretório de memória para scan
|
|
44
|
+
days_threshold: Dias mínimos para arquivar
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Lista de memórias candidatas
|
|
48
|
+
"""
|
|
49
|
+
candidates = []
|
|
50
|
+
now = datetime.now()
|
|
51
|
+
cutoff_ts = (now.timestamp()) - (days_threshold * 24 * 60 * 60)
|
|
52
|
+
|
|
53
|
+
if not memory_dir.exists():
|
|
54
|
+
return candidates
|
|
55
|
+
|
|
56
|
+
# Scan de arquivos .md (excluindo MEMORY.md)
|
|
57
|
+
for file_path in memory_dir.rglob("*.md"):
|
|
58
|
+
if file_path.name == "MEMORY.md":
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Usa mtime (última modificação) em vez de created_at
|
|
63
|
+
mtime = file_path.stat().st_mtime
|
|
64
|
+
|
|
65
|
+
if mtime < cutoff_ts:
|
|
66
|
+
# Verifica tipo no frontmatter
|
|
67
|
+
content = file_path.read_text(encoding="utf-8")[:2000]
|
|
68
|
+
frontmatter = parse_frontmatter(content)
|
|
69
|
+
mem_type = frontmatter.get("type")
|
|
70
|
+
|
|
71
|
+
# Nunca arquiva memórias de tipo 'user' ou 'feedback'
|
|
72
|
+
if mem_type in ['user', 'feedback']:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
candidates.append({
|
|
76
|
+
"file_path": str(file_path),
|
|
77
|
+
"filename": file_path.name,
|
|
78
|
+
"type": mem_type,
|
|
79
|
+
"name": frontmatter.get("name"),
|
|
80
|
+
"description": frontmatter.get("description"),
|
|
81
|
+
"mtime": mtime,
|
|
82
|
+
"days_since_modified": int((now.timestamp() - mtime) / (24 * 60 * 60))
|
|
83
|
+
})
|
|
84
|
+
except Exception:
|
|
85
|
+
# Silenciosamente ignora arquivos com erro
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
return candidates
|
|
89
|
+
|
|
90
|
+
def find_candidates_for_deletion(
|
|
91
|
+
self,
|
|
92
|
+
candidates: List[Dict[str, Any]],
|
|
93
|
+
deletion_threshold_days: int = 30
|
|
94
|
+
) -> List[Dict[str, Any]]:
|
|
95
|
+
"""
|
|
96
|
+
Encontra memórias candidatas para deleção.
|
|
97
|
+
|
|
98
|
+
Critérios:
|
|
99
|
+
- mtime > deletion_threshold_days (mais antigo que threshold)
|
|
100
|
+
- Tipo NÃO é 'user', 'feedback', ou 'project' com atividade recente
|
|
101
|
+
- Não está linkada no MEMORY.md
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
candidates: Lista de memórias candidatas (do find_candidates_for_archive)
|
|
105
|
+
deletion_threshold_days: Dias para deleção (default: 30)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Lista de memórias candidatas para deleção
|
|
109
|
+
"""
|
|
110
|
+
deletion_candidates = []
|
|
111
|
+
now = datetime.now()
|
|
112
|
+
deletion_cutoff = now.timestamp() - (deletion_threshold_days * 24 * 60 * 60)
|
|
113
|
+
|
|
114
|
+
for memory in candidates:
|
|
115
|
+
mtime = memory.get("mtime", 0)
|
|
116
|
+
mem_type = memory.get("type")
|
|
117
|
+
|
|
118
|
+
# Nunca deleta user ou feedback
|
|
119
|
+
if mem_type in ['user', 'feedback']:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# Deleta se mtime > threshold
|
|
123
|
+
if mtime < deletion_cutoff:
|
|
124
|
+
deletion_candidates.append(memory)
|
|
125
|
+
|
|
126
|
+
return deletion_candidates
|
|
127
|
+
|
|
128
|
+
def log_gc_event(
|
|
129
|
+
self,
|
|
130
|
+
action: str,
|
|
131
|
+
memory_id: str,
|
|
132
|
+
reason: str,
|
|
133
|
+
log_path: Path
|
|
134
|
+
) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Loga evento de GC.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
action: Ação realizada (archive, delete)
|
|
140
|
+
memory_id: ID da memória
|
|
141
|
+
reason: Motivo da ação
|
|
142
|
+
log_path: Path para arquivo de log
|
|
143
|
+
"""
|
|
144
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
145
|
+
log_entry = f"{timestamp} | {action} | {memory_id} | {reason}\n"
|
|
146
|
+
|
|
147
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
149
|
+
f.write(log_entry)
|
|
150
|
+
|
|
151
|
+
def run_gc(
|
|
152
|
+
self,
|
|
153
|
+
memory_dir: Path,
|
|
154
|
+
archive_threshold_days: int = 7,
|
|
155
|
+
deletion_threshold_days: int = 30,
|
|
156
|
+
dry_run: bool = True,
|
|
157
|
+
log_path: Optional[Path] = None
|
|
158
|
+
) -> Dict[str, Any]:
|
|
159
|
+
"""
|
|
160
|
+
Executa garbage collection.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
memory_dir: Diretório de memória para scan
|
|
164
|
+
archive_threshold_days: Dias para arquivamento (default: 7)
|
|
165
|
+
deletion_threshold_days: Dias para deleção (default: 30)
|
|
166
|
+
dry_run: Se True, apenas lista candidatas, não remove
|
|
167
|
+
log_path: Path para log (opcional)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Dicionário com resultados do GC
|
|
171
|
+
"""
|
|
172
|
+
results = {
|
|
173
|
+
"archive_candidates": [],
|
|
174
|
+
"deletion_candidates": [],
|
|
175
|
+
"archived": [],
|
|
176
|
+
"deleted": [],
|
|
177
|
+
"dry_run": dry_run
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Passo 1: Encontra candidatas para arquivamento
|
|
181
|
+
archive_candidates = self.find_candidates_for_archive(
|
|
182
|
+
memory_dir, archive_threshold_days
|
|
183
|
+
)
|
|
184
|
+
results["archive_candidates"] = [c["filename"] for c in archive_candidates]
|
|
185
|
+
|
|
186
|
+
# Passo 2: Encontra candidatas para deleção
|
|
187
|
+
deletion_candidates = self.find_candidates_for_deletion(
|
|
188
|
+
archive_candidates, deletion_threshold_days
|
|
189
|
+
)
|
|
190
|
+
results["deletion_candidates"] = [c["filename"] for c in deletion_candidates]
|
|
191
|
+
|
|
192
|
+
# Passo 3: Aplica GC (se não for dry_run)
|
|
193
|
+
if not dry_run:
|
|
194
|
+
for candidate in deletion_candidates:
|
|
195
|
+
try:
|
|
196
|
+
file_path = Path(candidate["file_path"])
|
|
197
|
+
file_path.unlink()
|
|
198
|
+
results["deleted"].append(candidate["filename"])
|
|
199
|
+
|
|
200
|
+
if log_path:
|
|
201
|
+
self.log_gc_event(
|
|
202
|
+
"delete",
|
|
203
|
+
candidate["filename"],
|
|
204
|
+
f"GC: {candidate['days_since_modified']} dias sem modificação",
|
|
205
|
+
log_path
|
|
206
|
+
)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
# Loga erro mas continua
|
|
209
|
+
if log_path:
|
|
210
|
+
self.log_gc_event("error", candidate["filename"], str(e), log_path)
|
|
211
|
+
|
|
212
|
+
# Arquiva as restantes (não deletadas)
|
|
213
|
+
import shutil
|
|
214
|
+
|
|
215
|
+
arquivo_dir = memory_dir / "arquivo"
|
|
216
|
+
arquivo_dir.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
|
|
218
|
+
memory_index = memory_dir / "MEMORY.md"
|
|
219
|
+
|
|
220
|
+
for candidate in archive_candidates:
|
|
221
|
+
if candidate["filename"] not in results["deleted"]:
|
|
222
|
+
src_path = Path(candidate["file_path"])
|
|
223
|
+
dst_path = arquivo_dir / src_path.name
|
|
224
|
+
try:
|
|
225
|
+
shutil.move(str(src_path), str(dst_path))
|
|
226
|
+
results["archived"].append(candidate["filename"])
|
|
227
|
+
|
|
228
|
+
# Remove referência do MEMORY.md
|
|
229
|
+
if memory_index.exists():
|
|
230
|
+
lines = memory_index.read_text(encoding="utf-8").splitlines()
|
|
231
|
+
updated = [
|
|
232
|
+
l for l in lines
|
|
233
|
+
if candidate["filename"] not in l
|
|
234
|
+
]
|
|
235
|
+
memory_index.write_text(
|
|
236
|
+
"\n".join(updated), encoding="utf-8"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if log_path:
|
|
240
|
+
self.log_gc_event(
|
|
241
|
+
"archive",
|
|
242
|
+
candidate["filename"],
|
|
243
|
+
f"GC: {candidate['days_since_modified']} dias sem modificação",
|
|
244
|
+
log_path
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
if log_path:
|
|
248
|
+
self.log_gc_event(
|
|
249
|
+
"error", candidate["filename"], str(e), log_path
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return results
|
|
253
|
+
|
|
254
|
+
def generate_gc_report(self, results: Dict[str, Any]) -> str:
|
|
255
|
+
"""
|
|
256
|
+
Gera relatório legível do GC.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
results: Dicionário de resultados do run_gc
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Relatório em markdown
|
|
263
|
+
"""
|
|
264
|
+
lines = [
|
|
265
|
+
"# Garbage Collection Report",
|
|
266
|
+
"",
|
|
267
|
+
f"**Modo:** {'Dry-run (nenhuma modificação)' if results['dry_run'] else 'Aplicação direta'}",
|
|
268
|
+
"",
|
|
269
|
+
"## Resumo",
|
|
270
|
+
"",
|
|
271
|
+
f"- Candidatas para arquivamento: {len(results['archive_candidates'])}",
|
|
272
|
+
f"- Candidatas para deleção: {len(results['deletion_candidates'])}",
|
|
273
|
+
f"- Arquivadas: {len(results['archived'])}",
|
|
274
|
+
f"- Deletadas: {len(results['deleted'])}",
|
|
275
|
+
"",
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
if results["archive_candidates"]:
|
|
279
|
+
lines.append("## Candidatas para Arquivamento")
|
|
280
|
+
lines.append("")
|
|
281
|
+
for filename in results["archive_candidates"]:
|
|
282
|
+
lines.append(f"- {filename}")
|
|
283
|
+
lines.append("")
|
|
284
|
+
|
|
285
|
+
if results["deletion_candidates"]:
|
|
286
|
+
lines.append("## Candidatas para Deleção")
|
|
287
|
+
lines.append("")
|
|
288
|
+
for filename in results["deletion_candidates"]:
|
|
289
|
+
lines.append(f"- {filename}")
|
|
290
|
+
lines.append("")
|
|
291
|
+
|
|
292
|
+
if not results["archive_candidates"] and not results["deletion_candidates"]:
|
|
293
|
+
lines.append("Nenhuma memória candidata para GC.")
|
|
294
|
+
lines.append("")
|
|
295
|
+
|
|
296
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Guard rails para forgetting do Cerebro"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GuardRails:
|
|
10
|
+
"""
|
|
11
|
+
Guard rails para políticas de forgetting.
|
|
12
|
+
|
|
13
|
+
Implementa regras de:
|
|
14
|
+
- never_delete: memórias que nunca podem ser deletadas
|
|
15
|
+
- always_archive: memórias que devem ser arquivadas após período
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config_path: Path):
|
|
19
|
+
"""
|
|
20
|
+
Inicializa o GuardRails.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config_path: Path para arquivo de configuração YAML
|
|
24
|
+
"""
|
|
25
|
+
self.config_path = config_path
|
|
26
|
+
self.config = self._load_config()
|
|
27
|
+
|
|
28
|
+
def _load_config(self) -> Dict:
|
|
29
|
+
"""
|
|
30
|
+
Carrega configuração.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Dicionário de configuração
|
|
34
|
+
"""
|
|
35
|
+
if not self.config_path.exists():
|
|
36
|
+
return {
|
|
37
|
+
"never_delete": ["decisions.critical", "errors.severity=high"],
|
|
38
|
+
"always_archive": {"raw": 30, "working": 90}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return yaml.safe_load(self.config_path.read_text())
|
|
42
|
+
|
|
43
|
+
def can_delete(self, memory: Dict[str, Any]) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Verifica se pode deletar uma memória.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
memory: Dados da memória
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True se pode deletar, False se está protegida
|
|
52
|
+
"""
|
|
53
|
+
rules = self.config.get("never_delete", [])
|
|
54
|
+
|
|
55
|
+
for rule in rules:
|
|
56
|
+
if self._matches_rule(memory, rule):
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def _matches_rule(self, memory: Dict[str, Any], rule: str) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Verifica se memória corresponde à regra.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
memory: Dados da memória
|
|
67
|
+
rule: Regra a verificar
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True se corresponde à regra
|
|
71
|
+
"""
|
|
72
|
+
if rule == "decisions.critical":
|
|
73
|
+
return memory.get("type") == "decision" and "critical" in memory.get("tags", [])
|
|
74
|
+
|
|
75
|
+
if rule == "errors.severity=high":
|
|
76
|
+
return memory.get("type") == "error" and memory.get("severity") in ["high", "critical"]
|
|
77
|
+
|
|
78
|
+
if rule == "errors.impact=critical":
|
|
79
|
+
return memory.get("type") == "error" and memory.get("impact") == "critical"
|
|
80
|
+
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
def should_archive(self, memory: Dict[str, Any], days_threshold: int) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Verifica se deve arquivar uma memória.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
memory: Dados da memória
|
|
89
|
+
days_threshold: Dias mínimos para arquivar
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True se deve arquivar
|
|
93
|
+
"""
|
|
94
|
+
created = memory.get("created_at")
|
|
95
|
+
if not created:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
created_dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
|
|
99
|
+
days_old = (datetime.now(timezone.utc) - created_dt).days
|
|
100
|
+
|
|
101
|
+
return days_old > days_threshold
|
|
102
|
+
|
|
103
|
+
def get_archive_threshold(self, layer: str) -> int:
|
|
104
|
+
"""
|
|
105
|
+
Obtém threshold de arquivamento por camada.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
layer: Nome da camada (raw, working, official)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dias threshold para arquivamento
|
|
112
|
+
"""
|
|
113
|
+
thresholds = self.config.get("always_archive", {})
|
|
114
|
+
return thresholds.get(layer, 90)
|
|
115
|
+
|
|
116
|
+
def is_protected(self, memory: Dict[str, Any]) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Verifica se memória está protegida contra deleção.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
memory: Dados da memória
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True se está protegida
|
|
125
|
+
"""
|
|
126
|
+
return not self.can_delete(memory)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Hooks do Cerebro: captura de eventos"""
|
|
2
|
+
from .core_captures import CoreCaptures
|
|
3
|
+
from .custom_loader import HooksLoader, HookRunner, HookConfig, create_sample_hooks_config
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"CoreCaptures",
|
|
7
|
+
"HooksLoader",
|
|
8
|
+
"HookRunner",
|
|
9
|
+
"HookConfig",
|
|
10
|
+
"create_sample_hooks_config"
|
|
11
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Captura de eventos core do Cerebro"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from src.core.jsonl_storage import JSONLStorage
|
|
6
|
+
from src.core.event_schema import Event, EventType, EventOrigin
|
|
7
|
+
from src.hooks.custom_loader import HooksLoader, HookRunner
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CoreCaptures:
|
|
11
|
+
"""
|
|
12
|
+
Captura de eventos core do Cerebro.
|
|
13
|
+
|
|
14
|
+
Fornece métodos para capturar:
|
|
15
|
+
- Tool calls (chamadas de ferramentas)
|
|
16
|
+
- Git events (eventos do git)
|
|
17
|
+
- Test results (resultados de testes)
|
|
18
|
+
- Errors (erros críticos)
|
|
19
|
+
|
|
20
|
+
Hooks customizados são executados automaticamente após cada captura.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
storage: JSONLStorage,
|
|
26
|
+
project: str,
|
|
27
|
+
session_id: str,
|
|
28
|
+
hooks_config_path: Optional[Path] = None
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Inicializa o CoreCaptures.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
storage: Instância do JSONLStorage
|
|
35
|
+
project: Nome do projeto
|
|
36
|
+
session_id: ID da sessão
|
|
37
|
+
hooks_config_path: Path para hooks.yaml (opcional)
|
|
38
|
+
"""
|
|
39
|
+
self.storage = storage
|
|
40
|
+
self.project = project
|
|
41
|
+
self.session_id = session_id
|
|
42
|
+
|
|
43
|
+
# Inicializa hooks loader e runner
|
|
44
|
+
if hooks_config_path is None:
|
|
45
|
+
hooks_config_path = Path("hooks.yaml")
|
|
46
|
+
|
|
47
|
+
self.hooks_loader = HooksLoader(hooks_config_path)
|
|
48
|
+
self.hooks_runner = HookRunner(self.hooks_loader, context={
|
|
49
|
+
"project": project,
|
|
50
|
+
"session_id": session_id
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
def _create_event(
|
|
54
|
+
self,
|
|
55
|
+
event_type: EventType,
|
|
56
|
+
subtype: str,
|
|
57
|
+
payload: Dict[str, Any],
|
|
58
|
+
tags: list = None
|
|
59
|
+
) -> Event:
|
|
60
|
+
"""
|
|
61
|
+
Cria e persiste evento, executando hooks customizados.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
event_type: Tipo do evento
|
|
65
|
+
subtype: Subtipo do evento
|
|
66
|
+
payload: Dados do evento
|
|
67
|
+
tags: Tags opcionais
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Evento criado
|
|
71
|
+
"""
|
|
72
|
+
event = Event(
|
|
73
|
+
project=self.project,
|
|
74
|
+
origin=EventOrigin.CLAUDE_CODE,
|
|
75
|
+
event_type=event_type,
|
|
76
|
+
subtype=subtype,
|
|
77
|
+
payload=payload,
|
|
78
|
+
tags=tags or [],
|
|
79
|
+
session_id=self.session_id
|
|
80
|
+
)
|
|
81
|
+
self.storage.append(event)
|
|
82
|
+
|
|
83
|
+
# Executa hooks customizados para este evento
|
|
84
|
+
self.hooks_runner.execute(event)
|
|
85
|
+
|
|
86
|
+
return event
|
|
87
|
+
|
|
88
|
+
def tool_call(self, tool: str, call_data: Dict[str, Any], result: Dict[str, Any]) -> Event:
|
|
89
|
+
"""
|
|
90
|
+
Captura tool call.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
tool: Nome da ferramenta
|
|
94
|
+
call_data: Dados da chamada
|
|
95
|
+
result: Resultado da execução
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Evento capturado
|
|
99
|
+
"""
|
|
100
|
+
return self._create_event(
|
|
101
|
+
EventType.TOOL_CALL,
|
|
102
|
+
tool,
|
|
103
|
+
{
|
|
104
|
+
"call": call_data,
|
|
105
|
+
"result": result
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def git_event(self, action: str, data: Dict[str, Any]) -> Event:
|
|
110
|
+
"""
|
|
111
|
+
Captura git event.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
action: Ação do git (commit, branch, merge, etc)
|
|
115
|
+
data: Dados do evento
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Evento capturado
|
|
119
|
+
"""
|
|
120
|
+
return self._create_event(
|
|
121
|
+
EventType.GIT_EVENT,
|
|
122
|
+
action,
|
|
123
|
+
data
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def test_result(self, test_type: str, test_name: str, status: str, duration: float, error: str = None) -> Event:
|
|
127
|
+
"""
|
|
128
|
+
Captura test result.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
test_type: Tipo de teste (unit, integration, e2e)
|
|
132
|
+
test_name: Nome do teste
|
|
133
|
+
status: Status (pass, fail, skip)
|
|
134
|
+
duration: Duração em segundos
|
|
135
|
+
error: Mensagem de erro (opcional)
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Evento capturado
|
|
139
|
+
"""
|
|
140
|
+
payload = {
|
|
141
|
+
"test_name": test_name,
|
|
142
|
+
"status": status,
|
|
143
|
+
"duration": duration
|
|
144
|
+
}
|
|
145
|
+
if error:
|
|
146
|
+
payload["error"] = error
|
|
147
|
+
|
|
148
|
+
return self._create_event(
|
|
149
|
+
EventType.TEST_RESULT,
|
|
150
|
+
test_type,
|
|
151
|
+
payload
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def error(self, error_type: str, context: Dict[str, Any]) -> Event:
|
|
155
|
+
"""
|
|
156
|
+
Captura erro.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
error_type: Tipo do erro
|
|
160
|
+
context: Contexto do erro
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Evento capturado
|
|
164
|
+
"""
|
|
165
|
+
return self._create_event(
|
|
166
|
+
EventType.ERROR,
|
|
167
|
+
error_type,
|
|
168
|
+
context,
|
|
169
|
+
tags=["error", "critical"]
|
|
170
|
+
)
|