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
package/src/cli/main.py
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"""CLI do Cerebro: comandos manuais"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from src.core.jsonl_storage import JSONLStorage
|
|
11
|
+
from src.core.session_manager import SessionManager
|
|
12
|
+
from src.working.yaml_storage import YAMLStorage
|
|
13
|
+
from src.working.memory_view import MemoryView
|
|
14
|
+
from src.official.markdown_storage import MarkdownStorage
|
|
15
|
+
from src.consolidation.checkpoints import CheckpointManager, CheckpointTrigger
|
|
16
|
+
from src.consolidation.extractor import Extractor
|
|
17
|
+
from src.consolidation.promoter import Promoter
|
|
18
|
+
from src.index.metadata_db import MetadataDB
|
|
19
|
+
from src.index.embeddings_db import EmbeddingsDB
|
|
20
|
+
from src.index.queries import QueryEngine
|
|
21
|
+
from src.diff.memory_diff import MemoryDiff, MemoryDiffResult
|
|
22
|
+
from src.consolidation.dream import run_dream, generate_dream_report
|
|
23
|
+
from src.consolidation.remember import run_remember, generate_remember_report
|
|
24
|
+
from src.forgetting.gc import GarbageCollector
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CerebroCLI:
|
|
28
|
+
"""
|
|
29
|
+
Interface de linha de comando do Cerebro.
|
|
30
|
+
|
|
31
|
+
Comandos disponíveis:
|
|
32
|
+
- checkpoint: Trigger manual de checkpoint
|
|
33
|
+
- memory: Visualizar memória ativa
|
|
34
|
+
- search: Buscar memórias
|
|
35
|
+
- promote: Promover draft para official
|
|
36
|
+
- gc: Garbage collection manual
|
|
37
|
+
- status: Status do sistema
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, cerebro_path: Path):
|
|
41
|
+
"""
|
|
42
|
+
Inicializa a CLI.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
cerebro_path: Diretório base do Cerebro
|
|
46
|
+
"""
|
|
47
|
+
self.cerebro_path = cerebro_path
|
|
48
|
+
self.cerebro_path.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
# Inicializa storages
|
|
51
|
+
self.raw_storage = JSONLStorage(cerebro_path / "raw")
|
|
52
|
+
self.working_storage = YAMLStorage(cerebro_path / "working")
|
|
53
|
+
self.official_storage = MarkdownStorage(cerebro_path / "official")
|
|
54
|
+
self.session_manager = SessionManager(cerebro_path)
|
|
55
|
+
|
|
56
|
+
# Inicializa índice
|
|
57
|
+
self.metadata_db = MetadataDB(cerebro_path / "index" / "metadata.db")
|
|
58
|
+
self.embeddings_db = EmbeddingsDB(cerebro_path / "index" / "embeddings.db")
|
|
59
|
+
self.query_engine = QueryEngine(self.metadata_db, self.embeddings_db)
|
|
60
|
+
|
|
61
|
+
# Inicializa consolidação
|
|
62
|
+
self.checkpoint_manager = CheckpointManager(cerebro_path / "config")
|
|
63
|
+
self.extractor = Extractor(self.raw_storage, self.working_storage)
|
|
64
|
+
self.promoter = Promoter(self.working_storage, self.official_storage)
|
|
65
|
+
|
|
66
|
+
# Inicializa memory view
|
|
67
|
+
self.memory_view = MemoryView(cerebro_path, self.official_storage, self.working_storage)
|
|
68
|
+
|
|
69
|
+
# Inicializa memory diff
|
|
70
|
+
self.memory_diff = MemoryDiff(
|
|
71
|
+
self.official_storage,
|
|
72
|
+
self.working_storage,
|
|
73
|
+
self.raw_storage
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def checkpoint(self, project: str, session_id: Optional[str] = None, reason: str = "manual") -> str:
|
|
77
|
+
"""
|
|
78
|
+
Trigger manual de checkpoint.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
project: Nome do projeto
|
|
82
|
+
session_id: ID da sessão (usa atual se None)
|
|
83
|
+
reason: Motivo do checkpoint
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Mensagem de resultado
|
|
87
|
+
"""
|
|
88
|
+
if session_id is None:
|
|
89
|
+
session_id = self.session_manager.get_session_id()
|
|
90
|
+
|
|
91
|
+
# Extrai sessão
|
|
92
|
+
try:
|
|
93
|
+
result = self.extractor.extract_session(project, session_id)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
return f"Erro ao extrair sessão: {e}"
|
|
96
|
+
|
|
97
|
+
if not result.events:
|
|
98
|
+
return f"Nenhum evento encontrado para sessão {session_id}"
|
|
99
|
+
|
|
100
|
+
# Cria draft
|
|
101
|
+
draft = self.extractor.create_draft(result, "session")
|
|
102
|
+
draft["checkpoint_reason"] = reason
|
|
103
|
+
|
|
104
|
+
# Escreve draft
|
|
105
|
+
draft_name = self.extractor.write_draft(project, draft, "session")
|
|
106
|
+
|
|
107
|
+
# Registra evento de checkpoint
|
|
108
|
+
from src.core.event_schema import Event, EventType, EventOrigin
|
|
109
|
+
checkpoint_event = Event(
|
|
110
|
+
project=project,
|
|
111
|
+
origin=EventOrigin.USER,
|
|
112
|
+
event_type=EventType.CHECKPOINT_CREATED,
|
|
113
|
+
subtype="manual",
|
|
114
|
+
payload={
|
|
115
|
+
"session_id": session_id,
|
|
116
|
+
"draft_name": draft_name,
|
|
117
|
+
"reason": reason,
|
|
118
|
+
"events_count": len(result.events)
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
self.raw_storage.append(checkpoint_event)
|
|
122
|
+
|
|
123
|
+
return f"Checkpoint criado: {draft_name} ({len(result.events)} eventos)"
|
|
124
|
+
|
|
125
|
+
def memory(self, project: str, output: Optional[Path] = None) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Gera visualização da memória ativa.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
project: Nome do projeto
|
|
131
|
+
output: Arquivo de saída (opcional)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Conteúdo do MEMORY.md
|
|
135
|
+
"""
|
|
136
|
+
content = self.memory_view.generate(project)
|
|
137
|
+
|
|
138
|
+
if output:
|
|
139
|
+
output.write_text(content, encoding="utf-8")
|
|
140
|
+
return f"MEMORY.md gerado em: {output}"
|
|
141
|
+
|
|
142
|
+
return content
|
|
143
|
+
|
|
144
|
+
def search(
|
|
145
|
+
self,
|
|
146
|
+
query: str,
|
|
147
|
+
project: Optional[str] = None,
|
|
148
|
+
mem_type: Optional[str] = None,
|
|
149
|
+
limit: int = 10,
|
|
150
|
+
use_semantic: bool = True
|
|
151
|
+
) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Busca memórias.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
query: Texto de busca
|
|
157
|
+
project: Filtrar por projeto
|
|
158
|
+
mem_type: Filtrar por tipo
|
|
159
|
+
limit: Limite de resultados
|
|
160
|
+
use_semantic: Usar busca semântica
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Resultados formatados
|
|
164
|
+
"""
|
|
165
|
+
results = self.query_engine.search(
|
|
166
|
+
query=query,
|
|
167
|
+
project=project,
|
|
168
|
+
mem_type=mem_type,
|
|
169
|
+
limit=limit,
|
|
170
|
+
use_semantic=use_semantic
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if not results:
|
|
174
|
+
return "Nenhum resultado encontrado."
|
|
175
|
+
|
|
176
|
+
lines = [f"Resultados para '{query}':\n"]
|
|
177
|
+
for i, r in enumerate(results, 1):
|
|
178
|
+
lines.append(f"{i}. [{r.type}] {r.title}")
|
|
179
|
+
lines.append(f" Projeto: {r.project} | Score: {r.score:.3f} | Fonte: {r.source}")
|
|
180
|
+
if r.metadata:
|
|
181
|
+
if r.metadata.get("tags"):
|
|
182
|
+
lines.append(f" Tags: {r.metadata['tags']}")
|
|
183
|
+
|
|
184
|
+
return "\n".join(lines)
|
|
185
|
+
|
|
186
|
+
def promote(
|
|
187
|
+
self,
|
|
188
|
+
project: str,
|
|
189
|
+
draft_id: str,
|
|
190
|
+
draft_type: str = "session",
|
|
191
|
+
promote_to: str = "decision"
|
|
192
|
+
) -> str:
|
|
193
|
+
"""
|
|
194
|
+
Promove draft para official.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
project: Nome do projeto
|
|
198
|
+
draft_id: ID do draft
|
|
199
|
+
draft_type: Tipo do draft
|
|
200
|
+
promote_to: Tipo de promoção
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Mensagem de resultado
|
|
204
|
+
"""
|
|
205
|
+
if draft_type == "session":
|
|
206
|
+
result = self.promoter.promote_session(project, draft_id, promote_to)
|
|
207
|
+
elif draft_type == "feature":
|
|
208
|
+
result = self.promoter.promote_feature(project, draft_id, promote_to)
|
|
209
|
+
else:
|
|
210
|
+
return f"Tipo de draft desconhecido: {draft_type}"
|
|
211
|
+
|
|
212
|
+
if result is None:
|
|
213
|
+
return "Draft não encontrado ou não pôde ser promovido."
|
|
214
|
+
|
|
215
|
+
if result.success:
|
|
216
|
+
# Marca como promovido
|
|
217
|
+
self.promoter.mark_promoted(project, draft_id, draft_type, result)
|
|
218
|
+
|
|
219
|
+
# Registra evento
|
|
220
|
+
from src.core.event_schema import Event, EventType, EventOrigin
|
|
221
|
+
promotion_event = Event(
|
|
222
|
+
project=project,
|
|
223
|
+
origin=EventOrigin.USER,
|
|
224
|
+
event_type=EventType.PROMOTION_PERFORMED,
|
|
225
|
+
subtype="manual",
|
|
226
|
+
payload={
|
|
227
|
+
"draft_id": draft_id,
|
|
228
|
+
"draft_type": draft_type,
|
|
229
|
+
"target_type": result.target_type,
|
|
230
|
+
"target_path": result.target_path
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
self.raw_storage.append(promotion_event)
|
|
234
|
+
|
|
235
|
+
return f"Promovido para: {result.target_path}"
|
|
236
|
+
else:
|
|
237
|
+
return f"Promoção falhou: {result.metadata.get('reason', 'desconhecido')}"
|
|
238
|
+
|
|
239
|
+
def gc(self, project: Optional[str] = None, dry_run: bool = True) -> str:
|
|
240
|
+
"""
|
|
241
|
+
Garbage collection manual.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
project: Nome do projeto (None para todos)
|
|
245
|
+
dry_run: Apenas simular
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Relatório de GC
|
|
249
|
+
"""
|
|
250
|
+
from src.forgetting.guard_rails import GuardRails
|
|
251
|
+
from src.forgetting.gc import GarbageCollector
|
|
252
|
+
|
|
253
|
+
guard_rails = GuardRails(self.cerebro_path / "config" / "cerebro.yaml")
|
|
254
|
+
gc = GarbageCollector(self.cerebro_path / "config")
|
|
255
|
+
|
|
256
|
+
# Lista memórias do índice
|
|
257
|
+
memories = self.metadata_db.search(project)
|
|
258
|
+
|
|
259
|
+
# Encontra candidatos para archive
|
|
260
|
+
archive_candidates = gc.find_candidates_for_archive(
|
|
261
|
+
memories,
|
|
262
|
+
guard_rails.get_archive_threshold("raw")
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Filtra por guard rails
|
|
266
|
+
delete_candidates = [
|
|
267
|
+
m for m in archive_candidates
|
|
268
|
+
if guard_rails.can_delete(m)
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
lines = ["Relatório de Garbage Collection:\n"]
|
|
272
|
+
lines.append(f"Total de memórias: {len(memories)}")
|
|
273
|
+
lines.append(f"Candidatas para archive: {len(archive_candidates)}")
|
|
274
|
+
lines.append(f"Candidatas para delete: {len(delete_candidates)}")
|
|
275
|
+
|
|
276
|
+
if dry_run:
|
|
277
|
+
lines.append("\n[DRY RUN] Nenhuma ação foi tomada.\n")
|
|
278
|
+
|
|
279
|
+
if delete_candidates:
|
|
280
|
+
lines.append("\nCandidatas para delete:")
|
|
281
|
+
for m in delete_candidates[:10]:
|
|
282
|
+
lines.append(f" - [{m['type']}] {m.get('title', m['id'])} (score: {m.get('total_score', 0):.3f})")
|
|
283
|
+
|
|
284
|
+
return "\n".join(lines)
|
|
285
|
+
|
|
286
|
+
def status(self) -> str:
|
|
287
|
+
"""
|
|
288
|
+
Status do sistema.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Relatório de status
|
|
292
|
+
"""
|
|
293
|
+
lines = ["Status do Cerebro:\n"]
|
|
294
|
+
|
|
295
|
+
# Session atual
|
|
296
|
+
session_id = self.session_manager.get_session_id()
|
|
297
|
+
lines.append(f"Session ID: {session_id}")
|
|
298
|
+
|
|
299
|
+
# Stats do raw
|
|
300
|
+
lines.append(f"\nRaw storage: {self.cerebro_path / 'raw'}")
|
|
301
|
+
|
|
302
|
+
# Stats do working
|
|
303
|
+
lines.append(f"Working storage: {self.cerebro_path / 'working'}")
|
|
304
|
+
|
|
305
|
+
# Stats do official
|
|
306
|
+
lines.append(f"Official storage: {self.cerebro_path / 'official'}")
|
|
307
|
+
|
|
308
|
+
# Stats do índice
|
|
309
|
+
try:
|
|
310
|
+
stats = self.metadata_db.search()
|
|
311
|
+
lines.append(f"\nÍndice: {len(stats)} memórias")
|
|
312
|
+
except Exception:
|
|
313
|
+
lines.append("\nÍndice: não disponível")
|
|
314
|
+
|
|
315
|
+
return "\n".join(lines)
|
|
316
|
+
|
|
317
|
+
def diff(
|
|
318
|
+
self,
|
|
319
|
+
project: str,
|
|
320
|
+
period_days: Optional[int] = None,
|
|
321
|
+
start_date: Optional[str] = None,
|
|
322
|
+
end_date: Optional[str] = None,
|
|
323
|
+
gc_threshold: float = 0.3,
|
|
324
|
+
output: Optional[Path] = None,
|
|
325
|
+
format: str = "markdown"
|
|
326
|
+
) -> str:
|
|
327
|
+
"""
|
|
328
|
+
Analisa diferenças de memória entre dois pontos no tempo.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
project: Nome do projeto
|
|
332
|
+
period_days: Dias do período (ex: 7, 30)
|
|
333
|
+
start_date: Data de início explícita (ISO string)
|
|
334
|
+
end_date: Data de fim explícita (ISO string)
|
|
335
|
+
gc_threshold: Threshold para garbage collection risk
|
|
336
|
+
output: Arquivo de saída (opcional)
|
|
337
|
+
format: Formato de saída (markdown, json)
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Relatório de Memory Diff
|
|
341
|
+
"""
|
|
342
|
+
result = self.memory_diff.analyze(
|
|
343
|
+
project=project,
|
|
344
|
+
period_days=period_days,
|
|
345
|
+
start_date=start_date,
|
|
346
|
+
end_date=end_date,
|
|
347
|
+
gc_threshold=gc_threshold
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
report = self.memory_diff.generate_report(result, format=format)
|
|
351
|
+
|
|
352
|
+
if output:
|
|
353
|
+
output.write_text(report, encoding="utf-8")
|
|
354
|
+
return f"Memory Diff report gerado em: {output}"
|
|
355
|
+
|
|
356
|
+
return report
|
|
357
|
+
|
|
358
|
+
def dream(self, since_days: int = 7, dry_run: bool = True) -> str:
|
|
359
|
+
"""
|
|
360
|
+
Extração automática de memórias.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
since_days: Dias para analisar
|
|
364
|
+
dry_run: Se True, apenas simula
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Relatório da extração
|
|
368
|
+
"""
|
|
369
|
+
from src.core.paths import get_auto_mem_path
|
|
370
|
+
from src.consolidation.dream import run_dream, generate_dream_report
|
|
371
|
+
|
|
372
|
+
memory_dir = get_auto_mem_path()
|
|
373
|
+
result = run_dream(memory_dir=memory_dir, since_days=since_days, dry_run=dry_run)
|
|
374
|
+
return generate_dream_report(result)
|
|
375
|
+
|
|
376
|
+
def remember(self, dry_run: bool = True) -> str:
|
|
377
|
+
"""
|
|
378
|
+
Revisão e promoção de memórias.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
dry_run: Se True, apenas gera relatório
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Relatório do remember
|
|
385
|
+
"""
|
|
386
|
+
from src.consolidation.remember import run_remember, generate_remember_report
|
|
387
|
+
|
|
388
|
+
report = run_remember(dry_run=dry_run)
|
|
389
|
+
return generate_remember_report(report)
|
|
390
|
+
|
|
391
|
+
def gc_cmd(self, threshold_days: int = 7, dry_run: bool = True) -> str:
|
|
392
|
+
"""
|
|
393
|
+
Garbage collection de memórias.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
threshold_days: Dias para considerar memória stale
|
|
397
|
+
dry_run: Se True, apenas lista candidatas
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Relatório do GC
|
|
401
|
+
"""
|
|
402
|
+
from src.core.paths import get_auto_mem_path
|
|
403
|
+
from src.forgetting.gc import GarbageCollector
|
|
404
|
+
|
|
405
|
+
memory_dir = get_auto_mem_path()
|
|
406
|
+
gc = GarbageCollector(memory_dir)
|
|
407
|
+
results = gc.run_gc(
|
|
408
|
+
memory_dir=memory_dir,
|
|
409
|
+
archive_threshold_days=threshold_days,
|
|
410
|
+
deletion_threshold_days=threshold_days * 4,
|
|
411
|
+
dry_run=dry_run
|
|
412
|
+
)
|
|
413
|
+
return gc.generate_gc_report(results)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def main():
|
|
417
|
+
"""Entry point da CLI"""
|
|
418
|
+
parser = argparse.ArgumentParser(
|
|
419
|
+
prog="ocerebro",
|
|
420
|
+
description="OCerebro - Sistema de Memoria para Agentes (Claude Code/MCP)"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
parser.add_argument(
|
|
424
|
+
"--cerebro-path",
|
|
425
|
+
type=Path,
|
|
426
|
+
default=Path(".ocerebro"),
|
|
427
|
+
help="Diretório base do OCerebro"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
subparsers = parser.add_subparsers(dest="command", help="Comandos")
|
|
431
|
+
|
|
432
|
+
# Comando: checkpoint
|
|
433
|
+
checkpoint_parser = subparsers.add_parser("checkpoint", help="Trigger manual de checkpoint")
|
|
434
|
+
checkpoint_parser.add_argument("project", help="Nome do projeto")
|
|
435
|
+
checkpoint_parser.add_argument("--session", help="ID da sessão")
|
|
436
|
+
checkpoint_parser.add_argument("--reason", default="manual", help="Motivo do checkpoint")
|
|
437
|
+
|
|
438
|
+
# Comando: memory
|
|
439
|
+
memory_parser = subparsers.add_parser("memory", help="Visualizar memória ativa")
|
|
440
|
+
memory_parser.add_argument("project", help="Nome do projeto")
|
|
441
|
+
memory_parser.add_argument("--output", type=Path, help="Arquivo de saída")
|
|
442
|
+
|
|
443
|
+
# Comando: search
|
|
444
|
+
search_parser = subparsers.add_parser("search", help="Buscar memórias")
|
|
445
|
+
search_parser.add_argument("query", help="Texto de busca")
|
|
446
|
+
search_parser.add_argument("--project", help="Filtrar por projeto")
|
|
447
|
+
search_parser.add_argument("--type", dest="mem_type", help="Filtrar por tipo")
|
|
448
|
+
search_parser.add_argument("--limit", type=int, default=10, help="Limite de resultados")
|
|
449
|
+
search_parser.add_argument("--no-semantic", action="store_true", help="Desativar busca semântica")
|
|
450
|
+
|
|
451
|
+
# Comando: promote
|
|
452
|
+
promote_parser = subparsers.add_parser("promote", help="Promover draft para official")
|
|
453
|
+
promote_parser.add_argument("project", help="Nome do projeto")
|
|
454
|
+
promote_parser.add_argument("draft_id", help="ID do draft")
|
|
455
|
+
promote_parser.add_argument("--type", default="session", help="Tipo do draft")
|
|
456
|
+
promote_parser.add_argument("--to", dest="promote_to", default="decision", help="Tipo de promoção")
|
|
457
|
+
|
|
458
|
+
# Comando: setup
|
|
459
|
+
setup_parser = subparsers.add_parser("setup", help="Configurar MCP Server e projeto automaticamente")
|
|
460
|
+
setup_parser.add_argument("subcommand", nargs="?", choices=["claude", "hooks", "init"], default="all",
|
|
461
|
+
help="O que configurar: claude (MCP), hooks (hooks.yaml), init (tudo)")
|
|
462
|
+
setup_parser.add_argument("--project", type=Path, help="Diretório do projeto (padrão: atual)")
|
|
463
|
+
|
|
464
|
+
# Comando: status
|
|
465
|
+
subparsers.add_parser("status", help="Status do sistema")
|
|
466
|
+
|
|
467
|
+
# Comando: diff
|
|
468
|
+
diff_parser = subparsers.add_parser("diff", help="Análise diferencial de memória entre dois pontos no tempo")
|
|
469
|
+
diff_parser.add_argument("project", help="Nome do projeto")
|
|
470
|
+
diff_parser.add_argument("--period", type=int, default=7, help="Dias do período (padrão: 7)")
|
|
471
|
+
diff_parser.add_argument("--start", dest="start_date", help="Data de início (ISO format, ex: 2026-03-01)")
|
|
472
|
+
diff_parser.add_argument("--end", dest="end_date", help="Data de fim (ISO format, ex: 2026-03-31)")
|
|
473
|
+
diff_parser.add_argument("--gc-threshold", type=float, default=0.3, help="Threshold para GC risk (padrão: 0.3)")
|
|
474
|
+
diff_parser.add_argument("--output", type=Path, help="Arquivo de saída")
|
|
475
|
+
diff_parser.add_argument("--format", choices=["markdown", "json"], default="markdown", help="Formato de saída")
|
|
476
|
+
|
|
477
|
+
# Comando: dream
|
|
478
|
+
dream_parser = subparsers.add_parser("dream", help="Extração automática de memórias")
|
|
479
|
+
dream_parser.add_argument("--since", type=int, default=7, dest="since_days", help="Dias para analisar (padrão: 7)")
|
|
480
|
+
dream_parser.add_argument("--apply", action="store_true", dest="apply", help="Aplicar extração (padrão: dry-run)")
|
|
481
|
+
|
|
482
|
+
# Comando: remember
|
|
483
|
+
remember_parser = subparsers.add_parser("remember", help="Revisão e promoção de memórias")
|
|
484
|
+
remember_parser.add_argument("--apply", action="store_true", dest="apply", help="Aplicar promoções (padrão: dry-run)")
|
|
485
|
+
|
|
486
|
+
# Comando: gc
|
|
487
|
+
gc_parser = subparsers.add_parser("gc", help="Garbage collection de memórias")
|
|
488
|
+
gc_parser.add_argument("--threshold", type=int, default=7, dest="threshold_days", help="Dias para considerar memória stale (padrão: 7)")
|
|
489
|
+
gc_parser.add_argument("--apply", action="store_true", dest="apply", help="Aplicar GC (padrão: dry-run)")
|
|
490
|
+
|
|
491
|
+
args = parser.parse_args()
|
|
492
|
+
|
|
493
|
+
if not args.command:
|
|
494
|
+
parser.print_help()
|
|
495
|
+
sys.exit(1)
|
|
496
|
+
|
|
497
|
+
# Comando setup nao precisa de CLI
|
|
498
|
+
if args.command == "setup":
|
|
499
|
+
from cerebro.cerebro_setup import setup_claude_desktop, setup_hooks, setup_cerebro_dir
|
|
500
|
+
|
|
501
|
+
if args.subcommand == "claude":
|
|
502
|
+
success = setup_claude_desktop()
|
|
503
|
+
sys.exit(0 if success else 1)
|
|
504
|
+
elif args.subcommand == "hooks":
|
|
505
|
+
success = setup_hooks(args.project)
|
|
506
|
+
sys.exit(0 if success else 1)
|
|
507
|
+
elif args.subcommand == "init":
|
|
508
|
+
# Pergunta: global ou projeto?
|
|
509
|
+
print("Como quer usar o OCerebro?")
|
|
510
|
+
print(" 1. Neste projeto (cria .ocerebro/ aqui)")
|
|
511
|
+
print(" 2. Global (usa ~/.ocerebro/ para todos os projetos)")
|
|
512
|
+
choice = input("\nEscolha [1/2] (padrão: 1): ").strip() or "1"
|
|
513
|
+
|
|
514
|
+
if choice == "2":
|
|
515
|
+
base_path = Path.home() / ".ocerebro"
|
|
516
|
+
print(f"\n✓ Modo global: {base_path}")
|
|
517
|
+
else:
|
|
518
|
+
base_path = Path.cwd() / ".ocerebro"
|
|
519
|
+
print(f"\n✓ Modo projeto: {base_path}")
|
|
520
|
+
|
|
521
|
+
# Salva a escolha num arquivo de config global
|
|
522
|
+
config_file = Path.home() / ".ocerebro_config"
|
|
523
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
524
|
+
config_file.write_text(f"base_path={base_path}\n", encoding="utf-8")
|
|
525
|
+
print(f"✓ Configuração salva em {config_file}")
|
|
526
|
+
|
|
527
|
+
setup_cerebro_dir(base_path)
|
|
528
|
+
setup_hooks(base_path)
|
|
529
|
+
print("\nSetup completo! Agora execute:")
|
|
530
|
+
print(" cerebro setup claude")
|
|
531
|
+
sys.exit(0)
|
|
532
|
+
else:
|
|
533
|
+
# Setup completo
|
|
534
|
+
setup_cerebro_dir(args.project)
|
|
535
|
+
setup_hooks(args.project)
|
|
536
|
+
setup_claude_desktop()
|
|
537
|
+
sys.exit(0)
|
|
538
|
+
|
|
539
|
+
# Inicializa CLI
|
|
540
|
+
cli = CerebroCLI(args.cerebro_path)
|
|
541
|
+
|
|
542
|
+
# Executa comando
|
|
543
|
+
if args.command == "checkpoint":
|
|
544
|
+
result = cli.checkpoint(args.project, args.session, args.reason)
|
|
545
|
+
elif args.command == "memory":
|
|
546
|
+
result = cli.memory(args.project, args.output)
|
|
547
|
+
elif args.command == "search":
|
|
548
|
+
result = cli.search(
|
|
549
|
+
args.query,
|
|
550
|
+
args.project,
|
|
551
|
+
args.mem_type,
|
|
552
|
+
args.limit,
|
|
553
|
+
not args.no_semantic
|
|
554
|
+
)
|
|
555
|
+
elif args.command == "promote":
|
|
556
|
+
result = cli.promote(args.project, args.draft_id, args.type, args.promote_to)
|
|
557
|
+
elif args.command == "status":
|
|
558
|
+
result = cli.status()
|
|
559
|
+
elif args.command == "diff":
|
|
560
|
+
result = cli.diff(
|
|
561
|
+
args.project,
|
|
562
|
+
period_days=args.period if not args.start_date else None,
|
|
563
|
+
start_date=args.start_date,
|
|
564
|
+
end_date=args.end_date,
|
|
565
|
+
gc_threshold=args.gc_threshold,
|
|
566
|
+
output=args.output,
|
|
567
|
+
format=args.format
|
|
568
|
+
)
|
|
569
|
+
elif args.command == "dream":
|
|
570
|
+
result = cli.dream(since_days=args.since_days, dry_run=not args.apply)
|
|
571
|
+
elif args.command == "remember":
|
|
572
|
+
result = cli.remember(dry_run=not args.apply)
|
|
573
|
+
elif args.command == "gc":
|
|
574
|
+
result = cli.gc_cmd(threshold_days=args.threshold_days, dry_run=not args.apply)
|
|
575
|
+
else:
|
|
576
|
+
parser.print_help()
|
|
577
|
+
sys.exit(1)
|
|
578
|
+
|
|
579
|
+
print(result)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
if __name__ == "__main__":
|
|
583
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Comando cerebro remember - Revisão e promoção de memórias.
|
|
2
|
+
|
|
3
|
+
Replica o comando /remember do Claude Code (bloqueado por USER_TYPE === 'ant').
|
|
4
|
+
|
|
5
|
+
Uso:
|
|
6
|
+
cerebro remember # Revisão (dry-run)
|
|
7
|
+
cerebro remember --apply # Aplica promoções automaticamente
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from src.consolidation.remember import run_remember, generate_remember_report
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_remember(
|
|
18
|
+
project_root: Optional[Path] = None,
|
|
19
|
+
dry_run: bool = True
|
|
20
|
+
) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Executa fluxo de revisão e promoção de memórias (remember).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
project_root: Raiz do projeto (default: git root)
|
|
26
|
+
dry_run: Se True, apenas gera relatório, não aplica
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Relatório do remember
|
|
30
|
+
"""
|
|
31
|
+
# Executa remember
|
|
32
|
+
report = run_remember(
|
|
33
|
+
project_root=project_root,
|
|
34
|
+
dry_run=dry_run
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Gera relatório
|
|
38
|
+
return generate_remember_report(report)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
"""Entry point do comando remember"""
|
|
43
|
+
parser = argparse.ArgumentParser(
|
|
44
|
+
prog="cerebro remember",
|
|
45
|
+
description="Revisão e promoção de memórias (replica /remember do Claude Code)"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--apply",
|
|
50
|
+
action="store_true",
|
|
51
|
+
dest="apply",
|
|
52
|
+
help="Aplicar promoções (padrão: dry-run)"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--project",
|
|
57
|
+
type=Path,
|
|
58
|
+
default=None,
|
|
59
|
+
dest="project_root",
|
|
60
|
+
help="Raiz do projeto (padrão: git root)"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
args = parser.parse_args()
|
|
64
|
+
|
|
65
|
+
result = cmd_remember(
|
|
66
|
+
project_root=args.project_root,
|
|
67
|
+
dry_run=not args.apply
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
print(result)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Consolidação do Cerebro: extração, scoring, promoção"""
|
|
2
|
+
from .checkpoints import CheckpointManager, CheckpointTrigger
|
|
3
|
+
from .scorer import Scorer, ScoringConfig
|
|
4
|
+
from .extractor import Extractor, ExtractionResult
|
|
5
|
+
from .promoter import Promoter, PromotionResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = ["CheckpointManager", "CheckpointTrigger", "Extractor", "ExtractionResult", "Scorer", "ScoringConfig", "Promoter", "PromotionResult"]
|