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,459 @@
|
|
|
1
|
+
"""Setup Automático do OCerebro
|
|
2
|
+
|
|
3
|
+
Detecta e configura o Claude Desktop e Claude Code automaticamente.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_all_claude_configs() -> dict:
|
|
14
|
+
"""Encontra todas as configurações do Claude (Desktop e Code).
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
dict com chaves "desktop" e "code", cada uma contendo Path | None
|
|
18
|
+
"""
|
|
19
|
+
result = {"desktop": None, "code": None}
|
|
20
|
+
|
|
21
|
+
# Claude Desktop: claude_desktop.json
|
|
22
|
+
desktop_locations = []
|
|
23
|
+
if sys.platform == "win32":
|
|
24
|
+
appdata = os.environ.get("APPDATA", "")
|
|
25
|
+
desktop_locations.extend([
|
|
26
|
+
Path(appdata) / "Claude" / "claude_desktop.json",
|
|
27
|
+
Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop.json",
|
|
28
|
+
Path.home() / ".claude" / "claude_desktop.json",
|
|
29
|
+
])
|
|
30
|
+
elif sys.platform == "darwin":
|
|
31
|
+
desktop_locations.extend([
|
|
32
|
+
Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop.json",
|
|
33
|
+
Path.home() / ".claude" / "claude_desktop.json",
|
|
34
|
+
])
|
|
35
|
+
elif sys.platform == "linux":
|
|
36
|
+
xdg_config = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
|
|
37
|
+
desktop_locations.extend([
|
|
38
|
+
Path(xdg_config) / "Claude" / "claude_desktop.json",
|
|
39
|
+
Path.home() / ".claude" / "claude_desktop.json",
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
# Claude Code: settings.json
|
|
43
|
+
code_locations = []
|
|
44
|
+
if sys.platform == "win32":
|
|
45
|
+
appdata = os.environ.get("APPDATA", "")
|
|
46
|
+
code_locations.extend([
|
|
47
|
+
Path(appdata) / "Claude" / "settings.json",
|
|
48
|
+
Path.home() / "AppData" / "Roaming" / "Claude" / "settings.json",
|
|
49
|
+
])
|
|
50
|
+
elif sys.platform == "darwin":
|
|
51
|
+
code_locations.extend([
|
|
52
|
+
Path.home() / "Library" / "Application Support" / "Claude" / "settings.json",
|
|
53
|
+
])
|
|
54
|
+
elif sys.platform == "linux":
|
|
55
|
+
xdg_config = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
|
|
56
|
+
code_locations.extend([
|
|
57
|
+
Path(xdg_config) / "Claude" / "settings.json",
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
# Universal (funciona para ambos)
|
|
61
|
+
universal_claude = Path.home() / ".claude"
|
|
62
|
+
if universal_claude.exists():
|
|
63
|
+
if result["desktop"] is None and (universal_claude / "claude_desktop.json").exists():
|
|
64
|
+
result["desktop"] = universal_claude / "claude_desktop.json"
|
|
65
|
+
if result["code"] is None and (universal_claude / "settings.json").exists():
|
|
66
|
+
result["code"] = universal_claude / "settings.json"
|
|
67
|
+
|
|
68
|
+
# Encontra primeiro desktop que existe
|
|
69
|
+
if result["desktop"] is None:
|
|
70
|
+
for loc in desktop_locations:
|
|
71
|
+
if loc.exists():
|
|
72
|
+
result["desktop"] = loc
|
|
73
|
+
break
|
|
74
|
+
else:
|
|
75
|
+
# Nenhum existe, retorna o primeiro para criar
|
|
76
|
+
result["desktop"] = desktop_locations[0] if desktop_locations else None
|
|
77
|
+
|
|
78
|
+
# Encontra primeiro code que existe
|
|
79
|
+
if result["code"] is None:
|
|
80
|
+
for loc in code_locations:
|
|
81
|
+
if loc.exists():
|
|
82
|
+
result["code"] = loc
|
|
83
|
+
break
|
|
84
|
+
else:
|
|
85
|
+
# Nenhum existe, retorna o primeiro para criar
|
|
86
|
+
result["code"] = code_locations[0] if code_locations else None
|
|
87
|
+
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def find_claude_desktop_config() -> Path | None:
|
|
92
|
+
"""Encontra o arquivo claude_desktop.json em várias localizações.
|
|
93
|
+
|
|
94
|
+
Legado: use find_all_claude_configs() para suporte completo.
|
|
95
|
+
"""
|
|
96
|
+
configs = find_all_claude_configs()
|
|
97
|
+
return configs.get("desktop")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_ocerebro_path() -> Path:
|
|
101
|
+
"""Retorna o caminho absoluto do OCerebro instalado"""
|
|
102
|
+
# Tenta encontrar o package instalado
|
|
103
|
+
import ocerebro
|
|
104
|
+
ocerebro_path = Path(ocerebro.__file__).parent
|
|
105
|
+
return ocerebro_path.resolve()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def generate_mcp_config(ocerebro_path: Path) -> dict:
|
|
109
|
+
"""Gera configuração MCP para o OCerebro com suporte robusto a paths.
|
|
110
|
+
|
|
111
|
+
SECURITY: Não salva API keys no config file.
|
|
112
|
+
As variáveis de ambiente são herdadas do sistema.
|
|
113
|
+
Configure no seu shell: ~/.bashrc ou ~/.zshrc
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
# Determina o comando Python
|
|
117
|
+
python_cmd = sys.executable
|
|
118
|
+
|
|
119
|
+
# Estratégia 1: usa python -m ocerebro.mcp (robusto para pip install)
|
|
120
|
+
mcp_config = {
|
|
121
|
+
"command": python_cmd,
|
|
122
|
+
"args": ["-m", "src.mcp.server"],
|
|
123
|
+
"cwd": str(ocerebro_path.parent),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Estratégia 2: path direto se arquivo existe
|
|
127
|
+
mcp_server = ocerebro_path / "src" / "mcp" / "server.py"
|
|
128
|
+
if not mcp_server.exists():
|
|
129
|
+
mcp_server = ocerebro_path.parent / "src" / "mcp" / "server.py"
|
|
130
|
+
|
|
131
|
+
if mcp_server.exists():
|
|
132
|
+
mcp_config["args"] = [str(mcp_server)]
|
|
133
|
+
|
|
134
|
+
# SECURITY: NÃO salvar API keys no config
|
|
135
|
+
# As variáveis de ambiente são herdadas automaticamente do shell
|
|
136
|
+
mcp_config["env"] = {}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"mcpServers": {
|
|
140
|
+
"ocerebro": mcp_config
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def backup_config(config_path: Path) -> Path | None:
|
|
146
|
+
"""Cria backup do arquivo de configuração"""
|
|
147
|
+
if not config_path.exists():
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
backup_path = config_path.with_suffix(".json.bak")
|
|
151
|
+
backup_path.write_text(config_path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
152
|
+
print(f"[OK] Backup criado: {backup_path}")
|
|
153
|
+
return backup_path
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def merge_configs(existing: dict, new: dict) -> dict:
|
|
157
|
+
"""Faz merge das configurações MCP"""
|
|
158
|
+
result = existing.copy()
|
|
159
|
+
|
|
160
|
+
if "mcpServers" not in result:
|
|
161
|
+
result["mcpServers"] = {}
|
|
162
|
+
|
|
163
|
+
# Adiciona/Atualiza servidor ocerebro
|
|
164
|
+
if "mcpServers" in new:
|
|
165
|
+
for name, config in new["mcpServers"].items():
|
|
166
|
+
result["mcpServers"][name] = config
|
|
167
|
+
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def setup_slash_commands(project_path: Path) -> bool:
|
|
172
|
+
"""Cria slash commands /cerebro no .claude/commands/ do projeto."""
|
|
173
|
+
|
|
174
|
+
commands_dir = project_path / ".claude" / "commands"
|
|
175
|
+
commands_dir.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
|
|
177
|
+
# cerebro-dream.md
|
|
178
|
+
dream_cmd = commands_dir / "cerebro-dream.md"
|
|
179
|
+
if not dream_cmd.exists():
|
|
180
|
+
dream_cmd.write_text("""---
|
|
181
|
+
description: Extrair memórias da sessão atual
|
|
182
|
+
---
|
|
183
|
+
Execute: ocerebro dream --since 7 --apply
|
|
184
|
+
Mostre o relatório completo do que foi salvo.
|
|
185
|
+
""", encoding="utf-8")
|
|
186
|
+
print(f"[OK] Slash command criado: {dream_cmd}")
|
|
187
|
+
|
|
188
|
+
# cerebro-status.md
|
|
189
|
+
status_cmd = commands_dir / "cerebro-status.md"
|
|
190
|
+
if not status_cmd.exists():
|
|
191
|
+
status_cmd.write_text("""---
|
|
192
|
+
description: Ver status da memória do projeto
|
|
193
|
+
---
|
|
194
|
+
Execute: ocerebro status
|
|
195
|
+
Liste quantas memórias existem por tipo.
|
|
196
|
+
""", encoding="utf-8")
|
|
197
|
+
print(f"[OK] Slash command criado: {status_cmd}")
|
|
198
|
+
|
|
199
|
+
# cerebro-gc.md
|
|
200
|
+
gc_cmd = commands_dir / "cerebro-gc.md"
|
|
201
|
+
if not gc_cmd.exists():
|
|
202
|
+
gc_cmd.write_text("""---
|
|
203
|
+
description: Limpeza de memórias antigas
|
|
204
|
+
---
|
|
205
|
+
Execute: ocerebro gc --threshold 30
|
|
206
|
+
Mostre o que será arquivado antes de confirmar.
|
|
207
|
+
""", encoding="utf-8")
|
|
208
|
+
print(f"[OK] Slash command criado: {gc_cmd}")
|
|
209
|
+
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def setup_claude_desktop() -> bool:
|
|
214
|
+
"""Configura o Claude Desktop ou Claude Code automaticamente."""
|
|
215
|
+
|
|
216
|
+
print("=" * 60)
|
|
217
|
+
print("OCerebro - Setup Automático")
|
|
218
|
+
print("=" * 60)
|
|
219
|
+
print()
|
|
220
|
+
|
|
221
|
+
# Encontra todas as configurações
|
|
222
|
+
configs = find_all_claude_configs()
|
|
223
|
+
|
|
224
|
+
# Pergunta qual versão o usuário usa
|
|
225
|
+
print("Qual versão do Claude você usa?")
|
|
226
|
+
print(" 1. Claude Desktop")
|
|
227
|
+
print(" 2. Claude Code (claude.ai/code)")
|
|
228
|
+
print(" 3. Ambos")
|
|
229
|
+
choice = input("\nEscolha [1/2/3] (padrão: 2): ").strip() or "2"
|
|
230
|
+
|
|
231
|
+
# Pega caminho do OCerebro
|
|
232
|
+
ocerebro_path = get_ocerebro_path()
|
|
233
|
+
print(f"\nOCerebro instalado: {ocerebro_path}")
|
|
234
|
+
|
|
235
|
+
# Gera nova configuração
|
|
236
|
+
new_config = generate_mcp_config(ocerebro_path)
|
|
237
|
+
|
|
238
|
+
configured = []
|
|
239
|
+
|
|
240
|
+
# Configura Claude Desktop se escolhido
|
|
241
|
+
if choice in ["1", "3"]:
|
|
242
|
+
config_path = configs.get("desktop")
|
|
243
|
+
if config_path:
|
|
244
|
+
print(f"\nConfig do Claude Desktop: {config_path}")
|
|
245
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
|
|
247
|
+
existing_config = {}
|
|
248
|
+
if config_path.exists():
|
|
249
|
+
print(f"Configuração existente encontrada")
|
|
250
|
+
backup_config(config_path)
|
|
251
|
+
try:
|
|
252
|
+
existing_config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
253
|
+
except json.JSONDecodeError as e:
|
|
254
|
+
print(f"Erro ao ler configuração existente: {e}")
|
|
255
|
+
existing_config = {}
|
|
256
|
+
|
|
257
|
+
merged_config = merge_configs(existing_config, new_config)
|
|
258
|
+
config_path.write_text(
|
|
259
|
+
json.dumps(merged_config, indent=2, ensure_ascii=False),
|
|
260
|
+
encoding="utf-8"
|
|
261
|
+
)
|
|
262
|
+
configured.append("Claude Desktop")
|
|
263
|
+
|
|
264
|
+
# Configura Claude Code se escolhido
|
|
265
|
+
if choice in ["2", "3"]:
|
|
266
|
+
config_path = configs.get("code")
|
|
267
|
+
if config_path:
|
|
268
|
+
print(f"\nConfig do Claude Code: {config_path}")
|
|
269
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
|
|
271
|
+
existing_config = {}
|
|
272
|
+
if config_path.exists():
|
|
273
|
+
print(f"Configuração existente encontrada")
|
|
274
|
+
backup_config(config_path)
|
|
275
|
+
try:
|
|
276
|
+
existing_config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
277
|
+
except json.JSONDecodeError as e:
|
|
278
|
+
print(f"Erro ao ler configuração existente: {e}")
|
|
279
|
+
existing_config = {}
|
|
280
|
+
|
|
281
|
+
merged_config = merge_configs(existing_config, new_config)
|
|
282
|
+
config_path.write_text(
|
|
283
|
+
json.dumps(merged_config, indent=2, ensure_ascii=False),
|
|
284
|
+
encoding="utf-8"
|
|
285
|
+
)
|
|
286
|
+
configured.append("Claude Code")
|
|
287
|
+
|
|
288
|
+
print()
|
|
289
|
+
print("[OK] OCerebro configurado em:", ", ".join(configured) if configured else "Nenhum")
|
|
290
|
+
print()
|
|
291
|
+
print("⚠️ API keys NÃO foram salvas no config.")
|
|
292
|
+
print(" Configure no seu shell:")
|
|
293
|
+
print(" export ANTHROPIC_AUTH_TOKEN=sua-key")
|
|
294
|
+
print()
|
|
295
|
+
print("Próximos passos:")
|
|
296
|
+
print(" 1. Reinicie o Claude (Desktop ou Code)")
|
|
297
|
+
print(" 2. As ferramentas do OCerebro estarão disponíveis:")
|
|
298
|
+
print(" - ocerebro_memory")
|
|
299
|
+
print(" - ocerebro_search")
|
|
300
|
+
print(" - ocerebro_checkpoint")
|
|
301
|
+
print(" - ocerebro_promote")
|
|
302
|
+
print(" - ocerebro_status")
|
|
303
|
+
print(" - ocerebro_hooks")
|
|
304
|
+
print(" - ocerebro_diff")
|
|
305
|
+
print(" - ocerebro_dream")
|
|
306
|
+
print(" - ocerebro_remember")
|
|
307
|
+
print(" - ocerebro_gc")
|
|
308
|
+
print()
|
|
309
|
+
print("=" * 60)
|
|
310
|
+
|
|
311
|
+
return True
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def setup_hooks(project_path: Path | None = None) -> bool:
|
|
315
|
+
"""Cria arquivo de exemplo hooks.yaml no projeto"""
|
|
316
|
+
|
|
317
|
+
if project_path is None:
|
|
318
|
+
project_path = Path.cwd()
|
|
319
|
+
|
|
320
|
+
hooks_yaml = project_path / "hooks.yaml"
|
|
321
|
+
|
|
322
|
+
if hooks_yaml.exists():
|
|
323
|
+
print(f" hooks.yaml já existe em {project_path}")
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
example_config = """# OCerebro Hooks Configuration
|
|
327
|
+
# Docs: https://github.com/OARANHA/ocerebro/blob/main/docs/HOOKS_GUIDE.md
|
|
328
|
+
|
|
329
|
+
hooks:
|
|
330
|
+
# Exemplo: Notificação de erros críticos
|
|
331
|
+
- name: error_notification
|
|
332
|
+
event_type: error
|
|
333
|
+
module_path: hooks/error_hook.py
|
|
334
|
+
function: on_error
|
|
335
|
+
config:
|
|
336
|
+
notify_severity: ["critical", "high"]
|
|
337
|
+
|
|
338
|
+
# Exemplo: Tracker de custo LLM
|
|
339
|
+
- name: llm_cost_tracker
|
|
340
|
+
event_type: tool_call
|
|
341
|
+
event_subtype: llm
|
|
342
|
+
module_path: hooks/cost_hook.py
|
|
343
|
+
function: on_llm_call
|
|
344
|
+
config:
|
|
345
|
+
monthly_budget: 100.0
|
|
346
|
+
alert_at_percentage: 80
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
hooks_yaml.write_text(example_config, encoding="utf-8")
|
|
350
|
+
|
|
351
|
+
# Cria diretório hooks/ com __init__.py
|
|
352
|
+
hooks_dir = project_path / "hooks"
|
|
353
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
354
|
+
(hooks_dir / "__init__.py").write_text('"""Hooks customizados do projeto"""', encoding="utf-8")
|
|
355
|
+
|
|
356
|
+
print(f"[OK] hooks.yaml criado em {project_path}")
|
|
357
|
+
print(f"[OK] Diretório hooks/ criado")
|
|
358
|
+
|
|
359
|
+
return True
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def setup_ocerebro_dir(project_path: Path | None = None) -> bool:
|
|
363
|
+
"""Cria diretório .ocerebro no projeto.
|
|
364
|
+
|
|
365
|
+
SECURITY: Valida path traversal - só permite paths dentro de home ou cwd.
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
if project_path is None:
|
|
369
|
+
project_path = Path.cwd()
|
|
370
|
+
|
|
371
|
+
# Validação de segurança: path deve ser dentro de home ou cwd
|
|
372
|
+
project_path = project_path.resolve()
|
|
373
|
+
home = Path.home().resolve()
|
|
374
|
+
cwd = Path.cwd().resolve()
|
|
375
|
+
|
|
376
|
+
if not (str(project_path).startswith(str(home)) or
|
|
377
|
+
str(project_path).startswith(str(cwd))):
|
|
378
|
+
print(f"❌ Erro: path '{project_path}' fora do diretório permitido.")
|
|
379
|
+
print(f" Permitido: dentro de {home} ou {cwd}")
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
ocerebro_dir = project_path / ".ocerebro"
|
|
383
|
+
|
|
384
|
+
if ocerebro_dir.exists():
|
|
385
|
+
print(f"[OK] Diretório .ocerebro já existe")
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
# Cria estrutura
|
|
389
|
+
(ocerebro_dir / "raw").mkdir(parents=True)
|
|
390
|
+
(ocerebro_dir / "working").mkdir(parents=True)
|
|
391
|
+
(ocerebro_dir / "official").mkdir(parents=True)
|
|
392
|
+
(ocerebro_dir / "index").mkdir(parents=True)
|
|
393
|
+
(ocerebro_dir / "config").mkdir(parents=True)
|
|
394
|
+
|
|
395
|
+
# Cria .gitignore dentro do .ocerebro
|
|
396
|
+
gitignore = ocerebro_dir / ".gitignore"
|
|
397
|
+
gitignore.write_text("""# Raw events (muito grandes)
|
|
398
|
+
raw/
|
|
399
|
+
|
|
400
|
+
# Working drafts (opcional sincronizar)
|
|
401
|
+
working/
|
|
402
|
+
|
|
403
|
+
# Index databases (regenerado automaticamente)
|
|
404
|
+
index/
|
|
405
|
+
|
|
406
|
+
# Config local
|
|
407
|
+
config/local.yaml
|
|
408
|
+
""", encoding="utf-8")
|
|
409
|
+
|
|
410
|
+
print(f"[OK] Diretório .ocerebro criado em {project_path}")
|
|
411
|
+
print(f" - raw/ (eventos brutos)")
|
|
412
|
+
print(f" - working/ (rascunhos)")
|
|
413
|
+
print(f" - official/ (memória permanente)")
|
|
414
|
+
print(f" - index/ (banco de dados)")
|
|
415
|
+
print(f" - config/ (configurações)")
|
|
416
|
+
|
|
417
|
+
return True
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def main():
|
|
421
|
+
"""Função principal de setup"""
|
|
422
|
+
|
|
423
|
+
if len(sys.argv) > 1:
|
|
424
|
+
subcommand = sys.argv[1]
|
|
425
|
+
|
|
426
|
+
if subcommand == "claude":
|
|
427
|
+
success = setup_claude_desktop()
|
|
428
|
+
sys.exit(0 if success else 1)
|
|
429
|
+
|
|
430
|
+
elif subcommand == "hooks":
|
|
431
|
+
project = Path(sys.argv[2]) if len(sys.argv) > 2 else None
|
|
432
|
+
success = setup_hooks(project)
|
|
433
|
+
sys.exit(0 if success else 1)
|
|
434
|
+
|
|
435
|
+
elif subcommand == "init":
|
|
436
|
+
project = Path(sys.argv[2]) if len(sys.argv) > 2 else Path.cwd()
|
|
437
|
+
setup_ocerebro_dir(project)
|
|
438
|
+
setup_hooks(project)
|
|
439
|
+
setup_slash_commands(project)
|
|
440
|
+
print()
|
|
441
|
+
print("Setup completo! Agora execute:")
|
|
442
|
+
print(" ocerebro setup claude")
|
|
443
|
+
sys.exit(0)
|
|
444
|
+
|
|
445
|
+
else:
|
|
446
|
+
print(f"Subcomando desconhecido: {subcommand}")
|
|
447
|
+
sys.exit(1)
|
|
448
|
+
|
|
449
|
+
# Setup completo padrão
|
|
450
|
+
print("Executando setup completo...")
|
|
451
|
+
print()
|
|
452
|
+
|
|
453
|
+
setup_ocerebro_dir()
|
|
454
|
+
setup_hooks()
|
|
455
|
+
setup_claude_desktop()
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
if __name__ == "__main__":
|
|
459
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Hooks customizados do Cerebro - Exemplos de implementação"""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Hook para tracker de custo de LLM"""
|
|
2
|
+
|
|
3
|
+
from src.core.event_schema import Event
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def on_llm_call(event: Event, context: dict, config: dict) -> dict:
|
|
7
|
+
"""
|
|
8
|
+
Trackea custo de chamadas LLM.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
event: Evento de tool_call LLM
|
|
12
|
+
context: Contexto global
|
|
13
|
+
config: Configuração do hook
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Informações de custo
|
|
17
|
+
"""
|
|
18
|
+
log_cost = config.get("log_cost", True)
|
|
19
|
+
monthly_budget = config.get("monthly_budget", 100.0)
|
|
20
|
+
alert_percentage = config.get("alert_at_percentage", 80)
|
|
21
|
+
|
|
22
|
+
payload = event.payload
|
|
23
|
+
model = payload.get("model", "unknown")
|
|
24
|
+
tokens = payload.get("tokens", {"input": 0, "output": 0})
|
|
25
|
+
cost = payload.get("cost", 0.0)
|
|
26
|
+
|
|
27
|
+
# Atualiza custo acumulado no contexto
|
|
28
|
+
accumulated = context.get("llm_cost_accumulated", 0.0)
|
|
29
|
+
context["llm_cost_accumulated"] = accumulated + cost
|
|
30
|
+
|
|
31
|
+
result = {
|
|
32
|
+
"model": model,
|
|
33
|
+
"tokens": tokens,
|
|
34
|
+
"current_cost": cost,
|
|
35
|
+
"accumulated_cost": accumulated + cost,
|
|
36
|
+
"budget": monthly_budget,
|
|
37
|
+
"budget_remaining": monthly_budget - (accumulated + cost),
|
|
38
|
+
"budget_used_percentage": ((accumulated + cost) / monthly_budget) * 100
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Alerta se aproximando do limite
|
|
42
|
+
if result["budget_used_percentage"] >= alert_percentage:
|
|
43
|
+
result["alert"] = f"Usou {result['budget_used_percentage']:.1f}% do budget mensal"
|
|
44
|
+
|
|
45
|
+
return result
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Hook para verificar cobertura de testes"""
|
|
2
|
+
|
|
3
|
+
from src.core.event_schema import Event
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def on_test_result(event: Event, context: dict, config: dict) -> dict:
|
|
7
|
+
"""
|
|
8
|
+
Verifica cobertura de testes após execução.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
event: Evento de resultado de teste
|
|
12
|
+
context: Contexto global
|
|
13
|
+
config: Configuração do hook
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Resultado da verificação
|
|
17
|
+
"""
|
|
18
|
+
min_coverage = config.get("min_coverage", 80)
|
|
19
|
+
fail_below = config.get("fail_below_threshold", False)
|
|
20
|
+
|
|
21
|
+
payload = event.payload
|
|
22
|
+
coverage = payload.get("coverage", 0)
|
|
23
|
+
|
|
24
|
+
result = {
|
|
25
|
+
"min_coverage": min_coverage,
|
|
26
|
+
"actual_coverage": coverage,
|
|
27
|
+
"passed": coverage >= min_coverage
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if coverage < min_coverage:
|
|
31
|
+
msg = f"Cobertura {coverage}% abaixo do mínimo {min_coverage}%"
|
|
32
|
+
if fail_below:
|
|
33
|
+
result["action"] = "fail_build"
|
|
34
|
+
result["message"] = msg
|
|
35
|
+
else:
|
|
36
|
+
result["action"] = "warn"
|
|
37
|
+
result["message"] = msg
|
|
38
|
+
|
|
39
|
+
return result
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Hook para notificação de erros"""
|
|
2
|
+
|
|
3
|
+
from src.core.event_schema import Event
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def on_error(event: Event, context: dict, config: dict) -> dict:
|
|
7
|
+
"""
|
|
8
|
+
Notifica erros críticos.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
event: Evento de erro
|
|
12
|
+
context: Contexto global
|
|
13
|
+
config: Configuração do hook
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Resultado da notificação
|
|
17
|
+
"""
|
|
18
|
+
notify_severity = config.get("notify_severity", ["critical", "high"])
|
|
19
|
+
channel = config.get("channel", "slack")
|
|
20
|
+
include_stacktrace = config.get("include_stacktrace", True)
|
|
21
|
+
|
|
22
|
+
payload = event.payload
|
|
23
|
+
severity = payload.get("severity", "medium")
|
|
24
|
+
error_type = payload.get("error_type", "Unknown")
|
|
25
|
+
message = payload.get("message", "")
|
|
26
|
+
|
|
27
|
+
result = {
|
|
28
|
+
"severity": severity,
|
|
29
|
+
"error_type": error_type,
|
|
30
|
+
"message": message,
|
|
31
|
+
"should_notify": severity in notify_severity
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if result["should_notify"]:
|
|
35
|
+
notification = {
|
|
36
|
+
"channel": channel,
|
|
37
|
+
"title": f"Erro {severity.upper()}: {error_type}",
|
|
38
|
+
"text": message
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if include_stacktrace and payload.get("stacktrace"):
|
|
42
|
+
notification["stacktrace"] = payload["stacktrace"]
|
|
43
|
+
|
|
44
|
+
result["notification"] = notification
|
|
45
|
+
# Aqui integraria com Slack/Teams/etc
|
|
46
|
+
# send_to_slack(notification)
|
|
47
|
+
|
|
48
|
+
return result
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Hook para log de operações custosas"""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from src.core.event_schema import Event
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def on_expensive_operation(event: Event, context: dict, config: dict) -> dict:
|
|
8
|
+
"""
|
|
9
|
+
Log de operações custosas.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
event: Evento de tool_call bash
|
|
13
|
+
context: Contexto global
|
|
14
|
+
config: Configuração do hook
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Informações da operação
|
|
18
|
+
"""
|
|
19
|
+
log_threshold = config.get("log_threshold_seconds", 5)
|
|
20
|
+
alert_threshold = config.get("alert_threshold_seconds", 30)
|
|
21
|
+
|
|
22
|
+
payload = event.payload
|
|
23
|
+
command = payload.get("command", "unknown")
|
|
24
|
+
duration = payload.get("duration", 0)
|
|
25
|
+
|
|
26
|
+
result = {
|
|
27
|
+
"command": command,
|
|
28
|
+
"duration": duration,
|
|
29
|
+
"logged": False,
|
|
30
|
+
"alerted": False
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if duration >= log_threshold:
|
|
34
|
+
result["logged"] = True
|
|
35
|
+
result["log_message"] = f"Operação lenta: {command} ({duration:.2f}s)"
|
|
36
|
+
|
|
37
|
+
if duration >= alert_threshold:
|
|
38
|
+
result["alerted"] = True
|
|
39
|
+
result["alert_message"] = f"ALERT: {command} levou {duration:.2f}s"
|
|
40
|
+
|
|
41
|
+
return result
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Hook para log global de eventos"""
|
|
2
|
+
|
|
3
|
+
from src.core.event_schema import Event
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def on_any_event(event: Event, context: dict, config: dict) -> dict:
|
|
7
|
+
"""
|
|
8
|
+
Log genérico para todos os eventos.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
event: Qualquer evento
|
|
12
|
+
context: Contexto global
|
|
13
|
+
config: Configuração do hook
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Informações do log
|
|
17
|
+
"""
|
|
18
|
+
log_level = config.get("log_level", "info")
|
|
19
|
+
exclude_subtypes = config.get("exclude_subtypes", [])
|
|
20
|
+
|
|
21
|
+
# Pula subtipos excluídos
|
|
22
|
+
if event.subtype in exclude_subtypes:
|
|
23
|
+
return {"skipped": True, "reason": f"subtype {event.subtype} excluído"}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"logged": True,
|
|
27
|
+
"log_level": log_level,
|
|
28
|
+
"event_type": event.event_type.value,
|
|
29
|
+
"subtype": event.subtype,
|
|
30
|
+
"project": event.project,
|
|
31
|
+
"session_id": event.session_id
|
|
32
|
+
}
|