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,640 @@
|
|
|
1
|
+
"""MCP Server do Cerebro: Integração com Claude Code"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from mcp.server import Server
|
|
9
|
+
from mcp.server.stdio import stdio_server
|
|
10
|
+
from mcp.types import Tool, TextContent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _safe_print_error(msg: str) -> None:
|
|
14
|
+
"""Print seguro para stderr com fallback de encoding."""
|
|
15
|
+
try:
|
|
16
|
+
print(msg, file=sys.stderr)
|
|
17
|
+
except UnicodeEncodeError:
|
|
18
|
+
print(msg.encode("ascii", errors="replace").decode("ascii"), file=sys.stderr)
|
|
19
|
+
|
|
20
|
+
from src.core.jsonl_storage import JSONLStorage
|
|
21
|
+
from src.core.session_manager import SessionManager
|
|
22
|
+
from src.working.yaml_storage import YAMLStorage
|
|
23
|
+
from src.working.memory_view import MemoryView
|
|
24
|
+
from src.official.markdown_storage import MarkdownStorage
|
|
25
|
+
from src.consolidation.extractor import Extractor
|
|
26
|
+
from src.consolidation.promoter import Promoter
|
|
27
|
+
from src.index.metadata_db import MetadataDB
|
|
28
|
+
from src.index.embeddings_db import EmbeddingsDB
|
|
29
|
+
from src.index.queries import QueryEngine
|
|
30
|
+
from src.hooks.custom_loader import HooksLoader, HookRunner
|
|
31
|
+
from src.diff.memory_diff import MemoryDiff
|
|
32
|
+
from src.consolidation.dream import run_dream, generate_dream_report
|
|
33
|
+
from src.consolidation.remember import run_remember, generate_remember_report
|
|
34
|
+
from src.forgetting.gc import GarbageCollector
|
|
35
|
+
from src.core.paths import get_auto_mem_path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CerebroMCP:
|
|
39
|
+
"""
|
|
40
|
+
MCP Server para integração do Cerebro com Claude Code.
|
|
41
|
+
|
|
42
|
+
Ferramentas disponíveis:
|
|
43
|
+
- cerebro_memory: Visualizar memória ativa
|
|
44
|
+
- cerebro_search: Buscar memórias
|
|
45
|
+
- cerebro_checkpoint: Criar checkpoint manual
|
|
46
|
+
- cerebro_promote: Promover draft para official
|
|
47
|
+
- cerebro_status: Status do sistema
|
|
48
|
+
- cerebro_hooks: Listar e gerenciar hooks customizados
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, cerebro_path: Optional[Path] = None):
|
|
52
|
+
"""
|
|
53
|
+
Inicializa o MCP Server.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
cerebro_path: Diretório base do OCerebro (default: .ocerebro)
|
|
57
|
+
"""
|
|
58
|
+
self.cerebro_path = cerebro_path or Path(".ocerebro")
|
|
59
|
+
self.cerebro_path.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
|
|
61
|
+
# Inicializa componentes
|
|
62
|
+
self.raw_storage = JSONLStorage(self.cerebro_path / "raw")
|
|
63
|
+
self.working_storage = YAMLStorage(self.cerebro_path / "working")
|
|
64
|
+
self.official_storage = MarkdownStorage(self.cerebro_path / "official")
|
|
65
|
+
self.session_manager = SessionManager(self.cerebro_path)
|
|
66
|
+
|
|
67
|
+
self.metadata_db = MetadataDB(self.cerebro_path / "index" / "metadata.db")
|
|
68
|
+
self.embeddings_db = EmbeddingsDB(self.cerebro_path / "index" / "embeddings.db")
|
|
69
|
+
self.query_engine = QueryEngine(self.metadata_db, self.embeddings_db)
|
|
70
|
+
|
|
71
|
+
self.extractor = Extractor(self.raw_storage, self.working_storage)
|
|
72
|
+
self.promoter = Promoter(self.working_storage, self.official_storage)
|
|
73
|
+
|
|
74
|
+
self.memory_view = MemoryView(
|
|
75
|
+
self.cerebro_path,
|
|
76
|
+
self.official_storage,
|
|
77
|
+
self.working_storage
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Inicializa memory diff
|
|
81
|
+
self.memory_diff = MemoryDiff(
|
|
82
|
+
self.official_storage,
|
|
83
|
+
self.working_storage,
|
|
84
|
+
self.raw_storage
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Inicializa hooks com tratamento de erro
|
|
88
|
+
# WARN-04 FIX: hooks.yaml com erro não derruba o servidor
|
|
89
|
+
hooks_config = self.cerebro_path.parent / "hooks.yaml"
|
|
90
|
+
try:
|
|
91
|
+
self.hooks_loader = HooksLoader(hooks_config) if hooks_config.exists() else None
|
|
92
|
+
self.hooks_runner = HookRunner(self.hooks_loader) if self.hooks_loader else None
|
|
93
|
+
except Exception as e:
|
|
94
|
+
# WINDOWS FIX: Usa _safe_print_error para evitar UnicodeEncodeError
|
|
95
|
+
_safe_print_error(f"[CEREBRO] Aviso: hooks.yaml com erro ({e}). Hooks desativados.")
|
|
96
|
+
self.hooks_loader = None
|
|
97
|
+
self.hooks_runner = None
|
|
98
|
+
|
|
99
|
+
def get_tools(self) -> List[Tool]:
|
|
100
|
+
"""
|
|
101
|
+
Retorna lista de ferramentas MCP.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Lista de ferramentas disponíveis
|
|
105
|
+
"""
|
|
106
|
+
return [
|
|
107
|
+
Tool(
|
|
108
|
+
name="cerebro_memory",
|
|
109
|
+
description="Visualizar memória ativa do projeto (MEMORY.md)",
|
|
110
|
+
inputSchema={
|
|
111
|
+
"type": "object",
|
|
112
|
+
"properties": {
|
|
113
|
+
"project": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"description": "Nome do projeto"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"required": ["project"]
|
|
119
|
+
}
|
|
120
|
+
),
|
|
121
|
+
Tool(
|
|
122
|
+
name="cerebro_search",
|
|
123
|
+
description="Buscar memórias por texto, tags ou metadados",
|
|
124
|
+
inputSchema={
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": {
|
|
127
|
+
"query": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"description": "Texto de busca"
|
|
130
|
+
},
|
|
131
|
+
"project": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"description": "Filtrar por projeto"
|
|
134
|
+
},
|
|
135
|
+
"type": {
|
|
136
|
+
"type": "string",
|
|
137
|
+
"description": "Tipo (decision, error, etc)"
|
|
138
|
+
},
|
|
139
|
+
"limit": {
|
|
140
|
+
"type": "integer",
|
|
141
|
+
"description": "Limite de resultados",
|
|
142
|
+
"default": 10
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"required": ["query"]
|
|
146
|
+
}
|
|
147
|
+
),
|
|
148
|
+
Tool(
|
|
149
|
+
name="cerebro_checkpoint",
|
|
150
|
+
description="Criar checkpoint manual de uma sessão",
|
|
151
|
+
inputSchema={
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {
|
|
154
|
+
"project": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"description": "Nome do projeto"
|
|
157
|
+
},
|
|
158
|
+
"session_id": {
|
|
159
|
+
"type": "string",
|
|
160
|
+
"description": "ID da sessão (opcional, usa atual)"
|
|
161
|
+
},
|
|
162
|
+
"reason": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"description": "Motivo do checkpoint",
|
|
165
|
+
"default": "manual"
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"required": ["project"]
|
|
169
|
+
}
|
|
170
|
+
),
|
|
171
|
+
Tool(
|
|
172
|
+
name="cerebro_promote",
|
|
173
|
+
description="Promover draft da working layer para official",
|
|
174
|
+
inputSchema={
|
|
175
|
+
"type": "object",
|
|
176
|
+
"properties": {
|
|
177
|
+
"project": {
|
|
178
|
+
"type": "string",
|
|
179
|
+
"description": "Nome do projeto"
|
|
180
|
+
},
|
|
181
|
+
"draft_id": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"description": "ID do draft"
|
|
184
|
+
},
|
|
185
|
+
"draft_type": {
|
|
186
|
+
"type": "string",
|
|
187
|
+
"description": "Tipo do draft",
|
|
188
|
+
"default": "session"
|
|
189
|
+
},
|
|
190
|
+
"promote_to": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"description": "Tipo de destino",
|
|
193
|
+
"default": "decision"
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"required": ["project", "draft_id"]
|
|
197
|
+
}
|
|
198
|
+
),
|
|
199
|
+
Tool(
|
|
200
|
+
name="cerebro_status",
|
|
201
|
+
description="Obter status do sistema Cerebro",
|
|
202
|
+
inputSchema={
|
|
203
|
+
"type": "object",
|
|
204
|
+
"properties": {}
|
|
205
|
+
}
|
|
206
|
+
),
|
|
207
|
+
Tool(
|
|
208
|
+
name="cerebro_hooks",
|
|
209
|
+
description="Listar e gerenciar hooks customizados",
|
|
210
|
+
inputSchema={
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {
|
|
213
|
+
"action": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"description": "Ação: list, test, info",
|
|
216
|
+
"enum": ["list", "test", "info"],
|
|
217
|
+
"default": "list"
|
|
218
|
+
},
|
|
219
|
+
"event_type": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"description": "Filtrar por tipo de evento"
|
|
222
|
+
},
|
|
223
|
+
"hook_name": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"description": "Nome do hook (para ação 'info')"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
),
|
|
230
|
+
Tool(
|
|
231
|
+
name="cerebro_diff",
|
|
232
|
+
description="Análise diferencial de memória entre dois pontos no tempo - mostra decisões adicionadas, erros documentados, drafts pendentes e memórias em risco de GC",
|
|
233
|
+
inputSchema={
|
|
234
|
+
"type": "object",
|
|
235
|
+
"properties": {
|
|
236
|
+
"project": {
|
|
237
|
+
"type": "string",
|
|
238
|
+
"description": "Nome do projeto"
|
|
239
|
+
},
|
|
240
|
+
"period_days": {
|
|
241
|
+
"type": "integer",
|
|
242
|
+
"description": "Dias do período (padrão: 7)",
|
|
243
|
+
"default": 7
|
|
244
|
+
},
|
|
245
|
+
"start_date": {
|
|
246
|
+
"type": "string",
|
|
247
|
+
"description": "Data de início (ISO format, ex: 2026-03-01)"
|
|
248
|
+
},
|
|
249
|
+
"end_date": {
|
|
250
|
+
"type": "string",
|
|
251
|
+
"description": "Data de fim (ISO format, ex: 2026-03-31)"
|
|
252
|
+
},
|
|
253
|
+
"gc_threshold": {
|
|
254
|
+
"type": "number",
|
|
255
|
+
"description": "Threshold para GC risk (padrão: 0.3)",
|
|
256
|
+
"default": 0.3
|
|
257
|
+
},
|
|
258
|
+
"format": {
|
|
259
|
+
"type": "string",
|
|
260
|
+
"description": "Formato de saída",
|
|
261
|
+
"enum": ["markdown", "json"],
|
|
262
|
+
"default": "markdown"
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"required": ["project"]
|
|
266
|
+
}
|
|
267
|
+
),
|
|
268
|
+
Tool(
|
|
269
|
+
name="cerebro_dream",
|
|
270
|
+
description="Extração automática de memórias (replica extractMemories do Claude Code) - analisa transcript e extrai memórias para user/feedback/project/reference",
|
|
271
|
+
inputSchema={
|
|
272
|
+
"type": "object",
|
|
273
|
+
"properties": {
|
|
274
|
+
"since_days": {
|
|
275
|
+
"type": "integer",
|
|
276
|
+
"description": "Dias para analisar (padrão: 7)",
|
|
277
|
+
"default": 7
|
|
278
|
+
},
|
|
279
|
+
"dry_run": {
|
|
280
|
+
"type": "boolean",
|
|
281
|
+
"description": "Se True, apenas simula (padrão: True)",
|
|
282
|
+
"default": True
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
),
|
|
287
|
+
Tool(
|
|
288
|
+
name="cerebro_remember",
|
|
289
|
+
description="Revisão e promoção de memórias (replica /remember do Claude Code) - classifica memórias por tipo e detecta duplicatas/conflitos entre camadas",
|
|
290
|
+
inputSchema={
|
|
291
|
+
"type": "object",
|
|
292
|
+
"properties": {
|
|
293
|
+
"dry_run": {
|
|
294
|
+
"type": "boolean",
|
|
295
|
+
"description": "Se True, apenas gera relatório (padrão: True)",
|
|
296
|
+
"default": True
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
),
|
|
301
|
+
Tool(
|
|
302
|
+
name="cerebro_gc",
|
|
303
|
+
description="Garbage collection de memórias - lista memórias stale por mtime e remove candidatas (nunca remove user/feedback)",
|
|
304
|
+
inputSchema={
|
|
305
|
+
"type": "object",
|
|
306
|
+
"properties": {
|
|
307
|
+
"threshold_days": {
|
|
308
|
+
"type": "integer",
|
|
309
|
+
"description": "Dias para considerar memória stale (padrão: 7)",
|
|
310
|
+
"default": 7
|
|
311
|
+
},
|
|
312
|
+
"dry_run": {
|
|
313
|
+
"type": "boolean",
|
|
314
|
+
"description": "Se True, apenas lista candidatas (padrão: True)",
|
|
315
|
+
"default": True
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
async def handle_tool(self, name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
323
|
+
"""
|
|
324
|
+
Processa chamada de ferramenta.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
name: Nome da ferramenta
|
|
328
|
+
arguments: Argumentos da chamada
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Resultado como lista de TextContent
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
if name == "cerebro_memory":
|
|
335
|
+
result = self._memory(arguments)
|
|
336
|
+
elif name == "cerebro_search":
|
|
337
|
+
result = self._search(arguments)
|
|
338
|
+
elif name == "cerebro_checkpoint":
|
|
339
|
+
result = self._checkpoint(arguments)
|
|
340
|
+
elif name == "cerebro_promote":
|
|
341
|
+
result = self._promote(arguments)
|
|
342
|
+
elif name == "cerebro_status":
|
|
343
|
+
result = self._status()
|
|
344
|
+
elif name == "cerebro_hooks":
|
|
345
|
+
result = self._hooks(arguments)
|
|
346
|
+
elif name == "cerebro_diff":
|
|
347
|
+
result = self._diff(arguments)
|
|
348
|
+
elif name == "cerebro_dream":
|
|
349
|
+
result = self._dream(arguments)
|
|
350
|
+
elif name == "cerebro_remember":
|
|
351
|
+
result = self._remember(arguments)
|
|
352
|
+
elif name == "cerebro_gc":
|
|
353
|
+
result = self._gc(arguments)
|
|
354
|
+
else:
|
|
355
|
+
return [TextContent(type="text", text=f"Ferramenta desconhecida: {name}")]
|
|
356
|
+
|
|
357
|
+
return [TextContent(type="text", text=result)]
|
|
358
|
+
except Exception as e:
|
|
359
|
+
return [TextContent(type="text", text=f"Erro: {str(e)}")]
|
|
360
|
+
|
|
361
|
+
def _memory(self, args: Dict[str, Any]) -> str:
|
|
362
|
+
"""Gera memória ativa"""
|
|
363
|
+
project = args.get("project")
|
|
364
|
+
if not project:
|
|
365
|
+
return "Erro: project é obrigatório"
|
|
366
|
+
|
|
367
|
+
return self.memory_view.generate(project)
|
|
368
|
+
|
|
369
|
+
def _search(self, args: Dict[str, Any]) -> str:
|
|
370
|
+
"""Busca memórias"""
|
|
371
|
+
query = args.get("query", "")
|
|
372
|
+
project = args.get("project")
|
|
373
|
+
mem_type = args.get("type")
|
|
374
|
+
limit = args.get("limit", 10)
|
|
375
|
+
|
|
376
|
+
results = self.query_engine.search(
|
|
377
|
+
query=query,
|
|
378
|
+
project=project,
|
|
379
|
+
mem_type=mem_type,
|
|
380
|
+
limit=limit
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
if not results:
|
|
384
|
+
return "Nenhum resultado encontrado."
|
|
385
|
+
|
|
386
|
+
lines = [f"Resultados para '{query}':\n"]
|
|
387
|
+
for i, r in enumerate(results, 1):
|
|
388
|
+
lines.append(f"{i}. [{r.type}] {r.title}")
|
|
389
|
+
lines.append(f" Projeto: {r.project} | Score: {r.score:.3f} | Fonte: {r.source}")
|
|
390
|
+
|
|
391
|
+
return "\n".join(lines)
|
|
392
|
+
|
|
393
|
+
def _checkpoint(self, args: Dict[str, Any]) -> str:
|
|
394
|
+
"""Cria checkpoint"""
|
|
395
|
+
project = args.get("project")
|
|
396
|
+
if not project:
|
|
397
|
+
return "Erro: project é obrigatório"
|
|
398
|
+
|
|
399
|
+
session_id = args.get("session_id") or self.session_manager.get_session_id()
|
|
400
|
+
reason = args.get("reason", "manual")
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
result = self.extractor.extract_session(project, session_id)
|
|
404
|
+
except Exception as e:
|
|
405
|
+
return f"Erro ao extrair sessão: {e}"
|
|
406
|
+
|
|
407
|
+
if not result.events:
|
|
408
|
+
return f"Nenhum evento encontrado para sessão {session_id}"
|
|
409
|
+
|
|
410
|
+
draft = self.extractor.create_draft(result, "session")
|
|
411
|
+
draft["checkpoint_reason"] = reason
|
|
412
|
+
draft_name = self.extractor.write_draft(project, draft, "session")
|
|
413
|
+
|
|
414
|
+
# Registra evento
|
|
415
|
+
from src.core.event_schema import Event, EventType, EventOrigin
|
|
416
|
+
checkpoint_event = Event(
|
|
417
|
+
project=project,
|
|
418
|
+
origin=EventOrigin.USER,
|
|
419
|
+
event_type=EventType.CHECKPOINT_CREATED,
|
|
420
|
+
subtype="mcp",
|
|
421
|
+
payload={
|
|
422
|
+
"session_id": session_id,
|
|
423
|
+
"draft_name": draft_name,
|
|
424
|
+
"reason": reason
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
self.raw_storage.append(checkpoint_event)
|
|
428
|
+
|
|
429
|
+
return f"Checkpoint criado: {draft_name} ({len(result.events)} eventos)"
|
|
430
|
+
|
|
431
|
+
def _promote(self, args: Dict[str, Any]) -> str:
|
|
432
|
+
"""Promove draft"""
|
|
433
|
+
project = args.get("project")
|
|
434
|
+
draft_id = args.get("draft_id")
|
|
435
|
+
|
|
436
|
+
if not project:
|
|
437
|
+
return "Erro: project é obrigatório"
|
|
438
|
+
if not draft_id:
|
|
439
|
+
return "Erro: draft_id é obrigatório"
|
|
440
|
+
|
|
441
|
+
draft_type = args.get("draft_type", "session")
|
|
442
|
+
promote_to = args.get("promote_to", "decision")
|
|
443
|
+
|
|
444
|
+
if draft_type == "session":
|
|
445
|
+
result = self.promoter.promote_session(project, draft_id, promote_to)
|
|
446
|
+
elif draft_type == "feature":
|
|
447
|
+
result = self.promoter.promote_feature(project, draft_id, promote_to)
|
|
448
|
+
else:
|
|
449
|
+
return f"Erro: Tipo de draft desconhecido: {draft_type}"
|
|
450
|
+
|
|
451
|
+
if result is None:
|
|
452
|
+
return "Draft não encontrado."
|
|
453
|
+
|
|
454
|
+
if result.success:
|
|
455
|
+
self.promoter.mark_promoted(project, draft_id, draft_type, result)
|
|
456
|
+
return f"Promovido para: {result.target_path}"
|
|
457
|
+
else:
|
|
458
|
+
return f"Promoção falhou: {result.metadata.get('reason', 'desconhecido')}"
|
|
459
|
+
|
|
460
|
+
def _status(self) -> str:
|
|
461
|
+
"""Status do sistema"""
|
|
462
|
+
session_id = self.session_manager.get_session_id()
|
|
463
|
+
|
|
464
|
+
lines = [
|
|
465
|
+
"Status do Cerebro:",
|
|
466
|
+
f" Session ID: {session_id}",
|
|
467
|
+
f" Path: {self.cerebro_path.absolute()}",
|
|
468
|
+
"",
|
|
469
|
+
"Storages:",
|
|
470
|
+
f" Raw: {self.cerebro_path / 'raw'}",
|
|
471
|
+
f" Working: {self.cerebro_path / 'working'}",
|
|
472
|
+
f" Official: {self.cerebro_path / 'official'}",
|
|
473
|
+
"",
|
|
474
|
+
"Índice:",
|
|
475
|
+
f" Metadata DB: {self.cerebro_path / 'index' / 'metadata.db'}",
|
|
476
|
+
f" Embeddings DB: {self.cerebro_path / 'index' / 'embeddings.db'}"
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
return "\n".join(lines)
|
|
480
|
+
|
|
481
|
+
def _hooks(self, args: Dict[str, Any]) -> str:
|
|
482
|
+
"""Lista e gerencia hooks customizados"""
|
|
483
|
+
action = args.get("action", "list")
|
|
484
|
+
event_type = args.get("event_type")
|
|
485
|
+
hook_name = args.get("hook_name")
|
|
486
|
+
|
|
487
|
+
if self.hooks_loader is None:
|
|
488
|
+
return "Hooks não configurados. Crie um arquivo hooks.yaml na raiz do projeto."
|
|
489
|
+
|
|
490
|
+
if action == "list":
|
|
491
|
+
hooks = self.hooks_loader.hooks
|
|
492
|
+
|
|
493
|
+
if event_type:
|
|
494
|
+
hooks = [h for h in hooks if h.event_type == event_type or h.event_type == "*"]
|
|
495
|
+
|
|
496
|
+
if not hooks:
|
|
497
|
+
return "Nenhum hook configurado."
|
|
498
|
+
|
|
499
|
+
lines = ["Hooks customizados configurados:\n"]
|
|
500
|
+
for i, hook in enumerate(hooks, 1):
|
|
501
|
+
lines.append(f"{i}. **{hook.name}**")
|
|
502
|
+
lines.append(f" - Evento: {hook.event_type}:{hook.event_subtype or '*'}")
|
|
503
|
+
lines.append(f" - Módulo: {hook.module_path}")
|
|
504
|
+
lines.append(f" - Função: {hook.function}")
|
|
505
|
+
if hook.config:
|
|
506
|
+
lines.append(f" - Config: {hook.config}")
|
|
507
|
+
lines.append("")
|
|
508
|
+
|
|
509
|
+
return "\n".join(lines)
|
|
510
|
+
|
|
511
|
+
elif action == "info":
|
|
512
|
+
if not hook_name:
|
|
513
|
+
return "Erro: hook_name é obrigatório para ação 'info'"
|
|
514
|
+
|
|
515
|
+
hook = next((h for h in self.hooks_loader.hooks if h.name == hook_name), None)
|
|
516
|
+
if not hook:
|
|
517
|
+
return f"Hook '{hook_name}' não encontrado."
|
|
518
|
+
|
|
519
|
+
lines = [
|
|
520
|
+
f"## Hook: {hook.name}",
|
|
521
|
+
"",
|
|
522
|
+
f"- **Evento:** {hook.event_type}:{hook.event_subtype or '*'}",
|
|
523
|
+
f"- **Módulo:** {hook.module_path}",
|
|
524
|
+
f"- **Função:** {hook.function}",
|
|
525
|
+
f"- **Config:** {hook.config or {}}",
|
|
526
|
+
"",
|
|
527
|
+
"### Assinatura da função",
|
|
528
|
+
"",
|
|
529
|
+
"```python",
|
|
530
|
+
f"def {hook.function}(event: Event, context: dict, config: dict) -> dict:",
|
|
531
|
+
" # Seu código aqui",
|
|
532
|
+
"```"
|
|
533
|
+
]
|
|
534
|
+
|
|
535
|
+
return "\n".join(lines)
|
|
536
|
+
|
|
537
|
+
elif action == "test":
|
|
538
|
+
# Simula execução de hook de teste
|
|
539
|
+
from src.core.event_schema import Event, EventType, EventOrigin
|
|
540
|
+
|
|
541
|
+
test_event = Event(
|
|
542
|
+
project="test",
|
|
543
|
+
origin=EventOrigin.CLAUDE_CODE,
|
|
544
|
+
event_type=EventType.TOOL_CALL,
|
|
545
|
+
subtype="bash",
|
|
546
|
+
payload={"command": "echo test", "duration": 0.1}
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
if self.hooks_runner is None:
|
|
550
|
+
return "Hooks runner não inicializado."
|
|
551
|
+
|
|
552
|
+
results = self.hooks_runner.execute(test_event)
|
|
553
|
+
|
|
554
|
+
lines = ["Resultado do teste de hooks:\n"]
|
|
555
|
+
for name, result in results.items():
|
|
556
|
+
status = "✅" if result["success"] else "❌"
|
|
557
|
+
lines.append(f"{status} {name}: {result.get('result', result.get('error', 'N/A'))}")
|
|
558
|
+
|
|
559
|
+
return "\n".join(lines)
|
|
560
|
+
|
|
561
|
+
return f"Ação desconhecida: {action}"
|
|
562
|
+
|
|
563
|
+
def _diff(self, args: Dict[str, Any]) -> str:
|
|
564
|
+
"""Análise diferencial de memória"""
|
|
565
|
+
project = args.get("project")
|
|
566
|
+
if not project:
|
|
567
|
+
return "Erro: project é obrigatório"
|
|
568
|
+
|
|
569
|
+
period_days = args.get("period_days", 7)
|
|
570
|
+
start_date = args.get("start_date")
|
|
571
|
+
end_date = args.get("end_date")
|
|
572
|
+
gc_threshold = args.get("gc_threshold", 0.3)
|
|
573
|
+
format = args.get("format", "markdown")
|
|
574
|
+
|
|
575
|
+
result = self.memory_diff.analyze(
|
|
576
|
+
project=project,
|
|
577
|
+
period_days=period_days if not start_date else None,
|
|
578
|
+
start_date=start_date,
|
|
579
|
+
end_date=end_date,
|
|
580
|
+
gc_threshold=gc_threshold
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
return self.memory_diff.generate_report(result, format=format)
|
|
584
|
+
|
|
585
|
+
def _dream(self, args: Dict[str, Any]) -> str:
|
|
586
|
+
"""Extração automática de memórias"""
|
|
587
|
+
since_days = args.get("since_days", 7)
|
|
588
|
+
dry_run = args.get("dry_run", True)
|
|
589
|
+
|
|
590
|
+
memory_dir = get_auto_mem_path()
|
|
591
|
+
result = run_dream(memory_dir=memory_dir, since_days=since_days, dry_run=dry_run)
|
|
592
|
+
return generate_dream_report(result)
|
|
593
|
+
|
|
594
|
+
def _remember(self, args: Dict[str, Any]) -> str:
|
|
595
|
+
"""Revisão e promoção de memórias"""
|
|
596
|
+
dry_run = args.get("dry_run", True)
|
|
597
|
+
|
|
598
|
+
report = run_remember(dry_run=dry_run)
|
|
599
|
+
return generate_remember_report(report)
|
|
600
|
+
|
|
601
|
+
def _gc(self, args: Dict[str, Any]) -> str:
|
|
602
|
+
"""Garbage collection de memórias"""
|
|
603
|
+
threshold_days = args.get("threshold_days", 7)
|
|
604
|
+
dry_run = args.get("dry_run", True)
|
|
605
|
+
|
|
606
|
+
memory_dir = get_auto_mem_path()
|
|
607
|
+
gc = GarbageCollector(memory_dir)
|
|
608
|
+
results = gc.run_gc(
|
|
609
|
+
memory_dir=memory_dir,
|
|
610
|
+
archive_threshold_days=threshold_days,
|
|
611
|
+
deletion_threshold_days=threshold_days * 4,
|
|
612
|
+
dry_run=dry_run
|
|
613
|
+
)
|
|
614
|
+
return gc.generate_gc_report(results)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
async def main():
|
|
618
|
+
"""Entry point do MCP Server"""
|
|
619
|
+
server = Server("cerebro")
|
|
620
|
+
cerebro = CerebroMCP()
|
|
621
|
+
|
|
622
|
+
@server.list_tools()
|
|
623
|
+
async def list_tools() -> List[Tool]:
|
|
624
|
+
return cerebro.get_tools()
|
|
625
|
+
|
|
626
|
+
@server.call_tool()
|
|
627
|
+
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
|
628
|
+
return await cerebro.handle_tool(name, arguments)
|
|
629
|
+
|
|
630
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
631
|
+
await server.run(
|
|
632
|
+
read_stream,
|
|
633
|
+
write_stream,
|
|
634
|
+
server.create_initialization_options()
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
if __name__ == "__main__":
|
|
639
|
+
import asyncio
|
|
640
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Memdir: sistema de memória do Claude Code"""
|
|
2
|
+
|
|
3
|
+
from src.memdir.scanner import (
|
|
4
|
+
MemoryHeader,
|
|
5
|
+
MemoryType,
|
|
6
|
+
scan_memory_files,
|
|
7
|
+
format_memory_manifest,
|
|
8
|
+
get_existing_memories_summary,
|
|
9
|
+
parse_frontmatter,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"MemoryHeader",
|
|
14
|
+
"MemoryType",
|
|
15
|
+
"scan_memory_files",
|
|
16
|
+
"format_memory_manifest",
|
|
17
|
+
"get_existing_memories_summary",
|
|
18
|
+
"parse_frontmatter",
|
|
19
|
+
]
|