ocerebro 0.1.8 → 0.2.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.
@@ -1,584 +1,575 @@
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 subprocess
9
- import sys
10
- from pathlib import Path
11
- from typing import Optional
12
-
13
-
14
- def find_all_claude_configs() -> dict:
15
- """Encontra todas as configurações do Claude (Desktop e Code).
16
-
17
- Returns:
18
- dict com chaves "desktop" e "code", cada uma contendo Path | None
19
- """
20
- result = {"desktop": None, "code": None}
21
-
22
- # Claude Desktop: claude_desktop.json
23
- desktop_locations = []
24
- if sys.platform == "win32":
25
- appdata = os.environ.get("APPDATA", "")
26
- desktop_locations.extend([
27
- Path(appdata) / "Claude" / "claude_desktop.json",
28
- Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop.json",
29
- Path.home() / ".claude" / "claude_desktop.json",
30
- ])
31
- elif sys.platform == "darwin":
32
- desktop_locations.extend([
33
- Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop.json",
34
- Path.home() / ".claude" / "claude_desktop.json",
35
- ])
36
- elif sys.platform == "linux":
37
- xdg_config = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
38
- desktop_locations.extend([
39
- Path(xdg_config) / "Claude" / "claude_desktop.json",
40
- Path.home() / ".claude" / "claude_desktop.json",
41
- ])
42
-
43
- # Claude Code: settings.json
44
- code_locations = []
45
- if sys.platform == "win32":
46
- appdata = os.environ.get("APPDATA", "")
47
- code_locations.extend([
48
- Path(appdata) / "Claude" / "settings.json",
49
- Path.home() / "AppData" / "Roaming" / "Claude" / "settings.json",
50
- ])
51
- elif sys.platform == "darwin":
52
- code_locations.extend([
53
- Path.home() / "Library" / "Application Support" / "Claude" / "settings.json",
54
- ])
55
- elif sys.platform == "linux":
56
- xdg_config = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
57
- code_locations.extend([
58
- Path(xdg_config) / "Claude" / "settings.json",
59
- ])
60
-
61
- # Universal (funciona para ambos)
62
- universal_claude = Path.home() / ".claude"
63
- if universal_claude.exists():
64
- if result["desktop"] is None and (universal_claude / "claude_desktop.json").exists():
65
- result["desktop"] = universal_claude / "claude_desktop.json"
66
- if result["code"] is None and (universal_claude / "settings.json").exists():
67
- result["code"] = universal_claude / "settings.json"
68
-
69
- # Encontra primeiro desktop que existe
70
- if result["desktop"] is None:
71
- for loc in desktop_locations:
72
- if loc.exists():
73
- result["desktop"] = loc
74
- break
75
- else:
76
- # Nenhum existe, retorna o primeiro para criar
77
- result["desktop"] = desktop_locations[0] if desktop_locations else None
78
-
79
- # Encontra primeiro code que existe
80
- if result["code"] is None:
81
- for loc in code_locations:
82
- if loc.exists():
83
- result["code"] = loc
84
- break
85
- else:
86
- # Nenhum existe, retorna o primeiro para criar
87
- result["code"] = code_locations[0] if code_locations else None
88
-
89
- return result
90
-
91
-
92
- def find_claude_desktop_config() -> Path | None:
93
- """Encontra o arquivo claude_desktop.json em várias localizações.
94
-
95
- Legado: use find_all_claude_configs() para suporte completo.
96
- """
97
- configs = find_all_claude_configs()
98
- return configs.get("desktop")
99
-
100
-
101
- def get_ocerebro_path() -> Path:
102
- """Retorna o caminho absoluto do OCerebro instalado"""
103
- # Tenta encontrar o package instalado via pip
104
- try:
105
- import cerebro
106
- cerebro_path = Path(cerebro.__file__).parent
107
- return cerebro_path.resolve()
108
- except ImportError:
109
- pass
110
-
111
- # Fallback: usa pip show para encontrar o Location
112
- try:
113
- result = subprocess.run(
114
- [sys.executable, "-m", "pip", "show", "ocerebro"],
115
- capture_output=True,
116
- text=True
117
- )
118
- if result.returncode == 0:
119
- for line in result.stdout.splitlines():
120
- if line.startswith("Location:"):
121
- return Path(line.split(":", 1)[1].strip())
122
- except Exception:
123
- pass
124
-
125
- # Último fallback: usa o path do próprio arquivo
126
- return Path(__file__).parent.resolve()
127
-
128
-
129
- def generate_mcp_config(ocerebro_path: Path) -> dict:
130
- """Gera configuração MCP para o OCerebro com suporte robusto a paths.
131
-
132
- SECURITY: Não salva API keys no config file.
133
- As variáveis de ambiente são herdadas do sistema.
134
- Configure no seu shell: ~/.bashrc ou ~/.zshrc
135
- """
136
-
137
- # Determina o comando Python
138
- python_cmd = sys.executable
139
-
140
- # Estratégia 1: usa python -m ocerebro.mcp (robusto para pip install)
141
- mcp_config = {
142
- "command": python_cmd,
143
- "args": ["-m", "src.mcp.server"],
144
- "cwd": str(ocerebro_path.parent),
145
- }
146
-
147
- # Estratégia 2: path direto se arquivo existe
148
- mcp_server = ocerebro_path / "src" / "mcp" / "server.py"
149
- if not mcp_server.exists():
150
- mcp_server = ocerebro_path.parent / "src" / "mcp" / "server.py"
151
-
152
- if mcp_server.exists():
153
- mcp_config["args"] = [str(mcp_server)]
154
-
155
- # SECURITY: NÃO salvar API keys no config
156
- # As variáveis de ambiente são herdadas automaticamente do shell
157
- mcp_config["env"] = {}
158
-
159
- return {
160
- "mcpServers": {
161
- "ocerebro": mcp_config
162
- }
163
- }
164
-
165
-
166
- def backup_config(config_path: Path) -> Path | None:
167
- """Cria backup do arquivo de configuração"""
168
- if not config_path.exists():
169
- return None
170
-
171
- backup_path = config_path.with_suffix(".json.bak")
172
- backup_path.write_text(config_path.read_text(encoding="utf-8"), encoding="utf-8")
173
- print(f"[OK] Backup criado: {backup_path}")
174
- return backup_path
175
-
176
-
177
- def merge_configs(existing: dict, new: dict) -> dict:
178
- """Faz merge das configurações MCP"""
179
- result = existing.copy()
180
-
181
- if "mcpServers" not in result:
182
- result["mcpServers"] = {}
183
-
184
- # Adiciona/Atualiza servidor ocerebro
185
- if "mcpServers" in new:
186
- for name, config in new["mcpServers"].items():
187
- result["mcpServers"][name] = config
188
-
189
- return result
190
-
191
-
192
- def setup_slash_commands(project_path: Path) -> bool:
193
- """Cria slash commands /cerebro no .claude/commands/ do projeto."""
194
-
195
- commands_dir = project_path / ".claude" / "commands"
196
- commands_dir.mkdir(parents=True, exist_ok=True)
197
-
198
- # cerebro-dream.md
199
- dream_cmd = commands_dir / "cerebro-dream.md"
200
- if not dream_cmd.exists():
201
- dream_cmd.write_text("""---
202
- description: Extrair memórias da sessão atual
203
- ---
204
- Execute: ocerebro dream --since 7 --apply
205
- Mostre o relatório completo do que foi salvo.
206
- """, encoding="utf-8")
207
- print(f"[OK] Slash command criado: {dream_cmd}")
208
-
209
- # cerebro-status.md
210
- status_cmd = commands_dir / "cerebro-status.md"
211
- if not status_cmd.exists():
212
- status_cmd.write_text("""---
213
- description: Ver status da memória do projeto
214
- ---
215
- Execute: ocerebro status
216
- Liste quantas memórias existem por tipo.
217
- """, encoding="utf-8")
218
- print(f"[OK] Slash command criado: {status_cmd}")
219
-
220
- # cerebro-gc.md
221
- gc_cmd = commands_dir / "cerebro-gc.md"
222
- if not gc_cmd.exists():
223
- gc_cmd.write_text("""---
224
- description: Limpeza de memórias antigas
225
- ---
226
- Execute: ocerebro gc --threshold 30
227
- Mostre o que será arquivado antes de confirmar.
228
- """, encoding="utf-8")
229
- print(f"[OK] Slash command criado: {gc_cmd}")
230
-
231
- return True
232
-
233
-
234
- def find_python_with_ocerebro() -> str:
235
- """Encontra o executável Python onde ocerebro está instalado.
236
-
237
- Prioridade:
238
- 1. sys.executable (Python atual)
239
- 2. python (PATH)
240
- 3. python3 (PATH)
241
-
242
- Returns:
243
- Caminho absoluto do Python ou None se não encontrado
244
- """
245
- candidates = []
246
-
247
- # Tenta sys.executable primeiro
248
- try:
249
- import ocerebro # noqa: F401
250
- candidates.append(sys.executable)
251
- except ImportError:
252
- pass
253
-
254
- # Tenta python e python3 no PATH
255
- for cmd in ["python", "python3"]:
256
- try:
257
- result = subprocess.run(
258
- [cmd, "-c", "import ocerebro; print('ok')"],
259
- capture_output=True,
260
- text=True,
261
- timeout=5
262
- )
263
- if result.returncode == 0:
264
- # Resolve full path
265
- result_path = subprocess.run(
266
- [cmd, "-c", "import sys; print(sys.executable)"],
267
- capture_output=True,
268
- text=True
269
- )
270
- if result_path.returncode == 0:
271
- candidates.append(result_path.stdout.strip())
272
- except Exception:
273
- continue
274
-
275
- return candidates[0] if candidates else sys.executable
276
-
277
-
278
- def get_claude_code_settings_path() -> Path | None:
279
- """Encontra o settings.json do Claude Code."""
280
- # Prioridade 1: ~/.claude/settings.json
281
- home_settings = Path.home() / ".claude" / "settings.json"
282
- if home_settings.exists():
283
- return home_settings
284
-
285
- # Prioridade 2: %APPDATA%/Claude/settings.json (Windows)
286
- if sys.platform == "win32":
287
- appdata = os.environ.get("APPDATA", "")
288
- if appdata:
289
- appdata_settings = Path(appdata) / "Claude" / "settings.json"
290
- if appdata_settings.exists():
291
- return appdata_settings
292
-
293
- # Retorna home_settings mesmo se não existe (para criar)
294
- return home_settings
295
-
296
-
297
- def get_claude_desktop_settings_path() -> Path | None:
298
- """Encontra o claude_desktop.json do Claude Desktop."""
299
- if sys.platform == "win32":
300
- appdata = os.environ.get("APPDATA", "")
301
- if appdata:
302
- return Path(appdata) / "Claude" / "claude_desktop.json"
303
- elif sys.platform == "darwin":
304
- return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop.json"
305
- return None
306
-
307
-
308
- def setup_claude(auto: bool = True) -> bool:
309
- """Configura MCP Server automaticamente.
310
-
311
- Args:
312
- auto: Se True, detecta automaticamente qual Claude usar.
313
- Se False, pergunta ao usuário.
314
-
315
- Returns:
316
- True se configurado com sucesso
317
- """
318
- print("=" * 60)
319
- print("OCerebro - Configurando MCP Server")
320
- print("=" * 60)
321
- print()
322
-
323
- # Encontra Python com ocerebro instalado
324
- python_cmd = find_python_with_ocerebro()
325
- print(f"[1/5] Python detectado: {python_cmd}")
326
-
327
- # Gera configuração MCP
328
- mcp_config = {
329
- "command": python_cmd,
330
- "args": ["-m", "src.mcp.server"],
331
- "cwd": str(Path(python_cmd).parent / "Lib" / "site-packages"),
332
- "env": {}
333
- }
334
- print(f"[2/5] Configuração MCP gerada")
335
-
336
- configured = []
337
- errors = []
338
-
339
- # Detecta qual Claude está instalado
340
- claude_code_path = get_claude_code_settings_path()
341
- claude_desktop_path = get_claude_desktop_settings_path()
342
-
343
- if auto:
344
- # Modo automático: configura ambos que existirem
345
- targets = []
346
- if claude_code_path and claude_code_path.exists():
347
- targets.append(("code", claude_code_path))
348
- if claude_desktop_path and claude_desktop_path.exists():
349
- targets.append(("desktop", claude_desktop_path))
350
-
351
- # Se nenhum existe, configura o default (Claude Code)
352
- if not targets:
353
- targets.append(("code", claude_code_path))
354
- else:
355
- # Modo interativo
356
- print("Qual versão do Claude você usa?")
357
- print(" 1. Claude Desktop")
358
- print(" 2. Claude Code (claude.ai/code)")
359
- print(" 3. Ambos")
360
- choice = input("\nEscolha [1/2/3] (padrão: 2): ").strip() or "2"
361
-
362
- targets = []
363
- if choice in ["1", "3"] and claude_desktop_path:
364
- targets.append(("desktop", claude_desktop_path))
365
- if choice in ["2", "3"] and claude_code_path:
366
- targets.append(("code", claude_code_path))
367
- if not targets:
368
- targets.append(("code", claude_code_path))
369
-
370
- # Configura cada target
371
- for target_type, config_path in targets:
372
- print(f"[3/5] Configurando {target_type}...")
373
-
374
- try:
375
- config_path.parent.mkdir(parents=True, exist_ok=True)
376
-
377
- existing_config = {}
378
- if config_path.exists():
379
- try:
380
- existing_config = json.loads(config_path.read_text(encoding="utf-8"))
381
- backup_config(config_path)
382
- except json.JSONDecodeError:
383
- print(f" Aviso: Config existente inválida, criando nova")
384
-
385
- # Merge das configurações
386
- if "mcpServers" not in existing_config:
387
- existing_config["mcpServers"] = {}
388
- existing_config["mcpServers"]["ocerebro"] = mcp_config
389
-
390
- # Garante que MCP está habilitado
391
- if "mcp" not in existing_config:
392
- existing_config["mcp"] = {}
393
- existing_config["mcp"]["enabled"] = True
394
-
395
- config_path.write_text(
396
- json.dumps(existing_config, indent=2, ensure_ascii=False),
397
- encoding="utf-8"
398
- )
399
-
400
- configured.append(target_type)
401
- print(f" [OK] {target_type}: {config_path}")
402
-
403
- except Exception as e:
404
- errors.append(f"{target_type}: {e}")
405
- print(f" [ERRO] {target_type}: {e}")
406
-
407
- # Resumo
408
- print()
409
- print("=" * 60)
410
- print("SETUP CONCLUÍDO!")
411
- print("=" * 60)
412
-
413
- if configured:
414
- print(f"\n[OK] MCP Server configurado em: {', '.join(configured)}")
415
- print("\nPróximos passos:")
416
- print(" 1. Reinicie o Claude (feche e abra novamente)")
417
- print(" 2. As ferramentas estarão disponíveis:")
418
- for tool in ["cerebro_memory", "cerebro_search", "cerebro_checkpoint",
419
- "cerebro_promote", "cerebro_status", "cerebro_hooks",
420
- "cerebro_diff", "cerebro_dream", "cerebro_remember", "cerebro_gc"]:
421
- print(f" - {tool}")
422
- print("\nPara testar, digite no Claude:")
423
- print(" /help (deve mostrar cerebro_*)")
424
- print(" ou: Use cerebro_status")
425
- else:
426
- print("\n[ERRO] Não foi possível configurar automaticamente.")
427
- print("Configure manualmente adicionando ao seu settings.json:")
428
- print(json.dumps({"mcpServers": {"ocerebro": mcp_config}}, indent=2))
429
-
430
- if errors:
431
- print(f"\nErros encontrados: {len(errors)}")
432
- for err in errors:
433
- print(f" - {err}")
434
-
435
- print()
436
- return len(configured) > 0
437
-
438
-
439
- def setup_hooks(project_path: Path | None = None) -> bool:
440
- """Cria arquivo de exemplo hooks.yaml no projeto"""
441
-
442
- if project_path is None:
443
- project_path = Path.cwd()
444
-
445
- hooks_yaml = project_path / "hooks.yaml"
446
-
447
- if hooks_yaml.exists():
448
- print(f" hooks.yaml já existe em {project_path}")
449
- return False
450
-
451
- example_config = """# OCerebro Hooks Configuration
452
- # Docs: https://github.com/OARANHA/ocerebro/blob/main/docs/HOOKS_GUIDE.md
453
-
454
- hooks:
455
- # Exemplo: Notificação de erros críticos
456
- - name: error_notification
457
- event_type: error
458
- module_path: hooks/error_hook.py
459
- function: on_error
460
- config:
461
- notify_severity: ["critical", "high"]
462
-
463
- # Exemplo: Tracker de custo LLM
464
- - name: llm_cost_tracker
465
- event_type: tool_call
466
- event_subtype: llm
467
- module_path: hooks/cost_hook.py
468
- function: on_llm_call
469
- config:
470
- monthly_budget: 100.0
471
- alert_at_percentage: 80
472
- """
473
-
474
- hooks_yaml.write_text(example_config, encoding="utf-8")
475
-
476
- # Cria diretório hooks/ com __init__.py
477
- hooks_dir = project_path / "hooks"
478
- hooks_dir.mkdir(exist_ok=True)
479
- (hooks_dir / "__init__.py").write_text('"""Hooks customizados do projeto"""', encoding="utf-8")
480
-
481
- print(f"[OK] hooks.yaml criado em {project_path}")
482
- print(f"[OK] Diretório hooks/ criado")
483
-
484
- return True
485
-
486
-
487
- def setup_ocerebro_dir(project_path: Path | None = None) -> bool:
488
- """Cria diretório .ocerebro no projeto.
489
-
490
- SECURITY: Valida path traversal - só permite paths dentro de home ou cwd.
491
- """
492
-
493
- if project_path is None:
494
- project_path = Path.cwd()
495
-
496
- # Validação de segurança: path deve ser dentro de home ou cwd
497
- project_path = project_path.resolve()
498
- home = Path.home().resolve()
499
- cwd = Path.cwd().resolve()
500
-
501
- if not (str(project_path).startswith(str(home)) or
502
- str(project_path).startswith(str(cwd))):
503
- print(f"❌ Erro: path '{project_path}' fora do diretório permitido.")
504
- print(f" Permitido: dentro de {home} ou {cwd}")
505
- return False
506
-
507
- ocerebro_dir = project_path / ".ocerebro"
508
-
509
- if ocerebro_dir.exists():
510
- print(f"[OK] Diretório .ocerebro já existe")
511
- return True
512
-
513
- # Cria estrutura
514
- (ocerebro_dir / "raw").mkdir(parents=True)
515
- (ocerebro_dir / "working").mkdir(parents=True)
516
- (ocerebro_dir / "official").mkdir(parents=True)
517
- (ocerebro_dir / "index").mkdir(parents=True)
518
- (ocerebro_dir / "config").mkdir(parents=True)
519
-
520
- # Cria .gitignore dentro do .ocerebro
521
- gitignore = ocerebro_dir / ".gitignore"
522
- gitignore.write_text("""# Raw events (muito grandes)
523
- raw/
524
-
525
- # Working drafts (opcional sincronizar)
526
- working/
527
-
528
- # Index databases (regenerado automaticamente)
529
- index/
530
-
531
- # Config local
532
- config/local.yaml
533
- """, encoding="utf-8")
534
-
535
- print(f"[OK] Diretório .ocerebro criado em {project_path}")
536
- print(f" - raw/ (eventos brutos)")
537
- print(f" - working/ (rascunhos)")
538
- print(f" - official/ (memória permanente)")
539
- print(f" - index/ (banco de dados)")
540
- print(f" - config/ (configurações)")
541
-
542
- return True
543
-
544
-
545
- def main():
546
- """Função principal de setup"""
547
-
548
- if len(sys.argv) > 1:
549
- subcommand = sys.argv[1]
550
-
551
- if subcommand == "claude":
552
- # Força reconfiguração do MCP
553
- success = setup_claude(auto=True)
554
- sys.exit(0 if success else 1)
555
-
556
- elif subcommand == "hooks":
557
- project = Path(sys.argv[2]) if len(sys.argv) > 2 else None
558
- success = setup_hooks(project)
559
- sys.exit(0 if success else 1)
560
-
561
- elif subcommand == "init":
562
- project = Path(sys.argv[2]) if len(sys.argv) > 2 else Path.cwd()
563
- setup_ocerebro_dir(project)
564
- setup_hooks(project)
565
- setup_slash_commands(project)
566
- # Auto-configura Claude
567
- setup_claude(auto=True)
568
- sys.exit(0)
569
-
570
- else:
571
- print(f"Subcomando desconhecido: {subcommand}")
572
- sys.exit(1)
573
-
574
- # Setup completo padrão
575
- print("Executando setup completo...")
576
- print()
577
-
578
- setup_ocerebro_dir()
579
- setup_hooks()
580
- setup_claude(auto=True)
581
-
582
-
583
- if __name__ == "__main__":
584
- main()
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 subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+
14
+ def find_all_claude_configs() -> dict:
15
+ """Encontra todas as configurações do Claude (Desktop e Code).
16
+
17
+ Returns:
18
+ dict com chaves "desktop" e "code", cada uma contendo Path | None
19
+ """
20
+ result = {"desktop": None, "code": None}
21
+
22
+ # Claude Desktop: claude_desktop.json
23
+ desktop_locations = []
24
+ if sys.platform == "win32":
25
+ appdata = os.environ.get("APPDATA", "")
26
+ desktop_locations.extend([
27
+ Path(appdata) / "Claude" / "claude_desktop.json",
28
+ Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop.json",
29
+ Path.home() / ".claude" / "claude_desktop.json",
30
+ ])
31
+ elif sys.platform == "darwin":
32
+ desktop_locations.extend([
33
+ Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop.json",
34
+ Path.home() / ".claude" / "claude_desktop.json",
35
+ ])
36
+ elif sys.platform == "linux":
37
+ xdg_config = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
38
+ desktop_locations.extend([
39
+ Path(xdg_config) / "Claude" / "claude_desktop.json",
40
+ Path.home() / ".claude" / "claude_desktop.json",
41
+ ])
42
+
43
+ # Claude Code: settings.json
44
+ code_locations = []
45
+ if sys.platform == "win32":
46
+ appdata = os.environ.get("APPDATA", "")
47
+ code_locations.extend([
48
+ Path(appdata) / "Claude" / "settings.json",
49
+ Path.home() / "AppData" / "Roaming" / "Claude" / "settings.json",
50
+ ])
51
+ elif sys.platform == "darwin":
52
+ code_locations.extend([
53
+ Path.home() / "Library" / "Application Support" / "Claude" / "settings.json",
54
+ ])
55
+ elif sys.platform == "linux":
56
+ xdg_config = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
57
+ code_locations.extend([
58
+ Path(xdg_config) / "Claude" / "settings.json",
59
+ ])
60
+
61
+ # Universal (funciona para ambos)
62
+ universal_claude = Path.home() / ".claude"
63
+ if universal_claude.exists():
64
+ if result["desktop"] is None and (universal_claude / "claude_desktop.json").exists():
65
+ result["desktop"] = universal_claude / "claude_desktop.json"
66
+ if result["code"] is None and (universal_claude / "settings.json").exists():
67
+ result["code"] = universal_claude / "settings.json"
68
+
69
+ # Encontra primeiro desktop que existe
70
+ if result["desktop"] is None:
71
+ for loc in desktop_locations:
72
+ if loc.exists():
73
+ result["desktop"] = loc
74
+ break
75
+ else:
76
+ # Nenhum existe, retorna o primeiro para criar
77
+ result["desktop"] = desktop_locations[0] if desktop_locations else None
78
+
79
+ # Encontra primeiro code que existe
80
+ if result["code"] is None:
81
+ for loc in code_locations:
82
+ if loc.exists():
83
+ result["code"] = loc
84
+ break
85
+ else:
86
+ # Nenhum existe, retorna o primeiro para criar
87
+ result["code"] = code_locations[0] if code_locations else None
88
+
89
+ return result
90
+
91
+
92
+ def find_claude_desktop_config() -> Path | None:
93
+ """Encontra o arquivo claude_desktop.json em várias localizações.
94
+
95
+ Legado: use find_all_claude_configs() para suporte completo.
96
+ """
97
+ configs = find_all_claude_configs()
98
+ return configs.get("desktop")
99
+
100
+
101
+ def get_ocerebro_path() -> Path:
102
+ """Retorna o caminho absoluto do OCerebro instalado"""
103
+ # Tenta encontrar o package instalado via pip
104
+ try:
105
+ import cerebro
106
+ cerebro_path = Path(cerebro.__file__).parent
107
+ return cerebro_path.resolve()
108
+ except ImportError:
109
+ pass
110
+
111
+ # Fallback: usa pip show para encontrar o Location
112
+ try:
113
+ result = subprocess.run(
114
+ [sys.executable, "-m", "pip", "show", "ocerebro"],
115
+ capture_output=True,
116
+ text=True
117
+ )
118
+ if result.returncode == 0:
119
+ for line in result.stdout.splitlines():
120
+ if line.startswith("Location:"):
121
+ return Path(line.split(":", 1)[1].strip())
122
+ except Exception:
123
+ pass
124
+
125
+ # Último fallback: usa o path do próprio arquivo
126
+ return Path(__file__).parent.resolve()
127
+
128
+
129
+ def generate_mcp_config(ocerebro_path: Path) -> dict:
130
+ """Gera configuração MCP para o OCerebro com suporte robusto a paths.
131
+
132
+ SECURITY: Não salva API keys no config file.
133
+ As variáveis de ambiente são herdadas do sistema.
134
+ Configure no seu shell: ~/.bashrc ou ~/.zshrc
135
+ """
136
+
137
+ # Determina o comando Python
138
+ python_cmd = sys.executable
139
+
140
+ # Estratégia 1: usa python -m ocerebro.mcp (robusto para pip install)
141
+ mcp_config = {
142
+ "command": python_cmd,
143
+ "args": ["-m", "src.mcp.server"],
144
+ "cwd": str(ocerebro_path.parent),
145
+ }
146
+
147
+ # Estratégia 2: path direto se arquivo existe
148
+ mcp_server = ocerebro_path / "src" / "mcp" / "server.py"
149
+ if not mcp_server.exists():
150
+ mcp_server = ocerebro_path.parent / "src" / "mcp" / "server.py"
151
+
152
+ if mcp_server.exists():
153
+ mcp_config["args"] = [str(mcp_server)]
154
+
155
+ # SECURITY: NÃO salvar API keys no config
156
+ mcp_config["env"] = {}
157
+
158
+ return {
159
+ "mcpServers": {
160
+ "ocerebro": mcp_config
161
+ }
162
+ }
163
+
164
+
165
+ def backup_config(config_path: Path) -> Path | None:
166
+ """Cria backup do arquivo de configuração"""
167
+ if not config_path.exists():
168
+ return None
169
+
170
+ backup_path = config_path.with_suffix(".json.bak")
171
+ backup_path.write_text(config_path.read_text(encoding="utf-8"), encoding="utf-8")
172
+ print(f"[OK] Backup criado: {backup_path}")
173
+ return backup_path
174
+
175
+
176
+ def merge_configs(existing: dict, new: dict) -> dict:
177
+ """Faz merge das configurações MCP"""
178
+ result = existing.copy()
179
+
180
+ if "mcpServers" not in result:
181
+ result["mcpServers"] = {}
182
+
183
+ if "mcpServers" in new:
184
+ for name, config in new["mcpServers"].items():
185
+ result["mcpServers"][name] = config
186
+
187
+ return result
188
+
189
+
190
+ def setup_slash_commands(project_path: Path | None = None, global_commands: bool = True) -> bool:
191
+ """Cria slash commands /cerebro no .claude/commands/ do projeto e global."""
192
+
193
+ if project_path:
194
+ commands_dir = project_path / ".claude" / "commands"
195
+ commands_dir.mkdir(parents=True, exist_ok=True)
196
+
197
+ dream_cmd = commands_dir / "cerebro-dream.md"
198
+ if not dream_cmd.exists():
199
+ dream_cmd.write_text("""---
200
+ description: Extrair memórias da sessão atual
201
+ ---
202
+ Execute: ocerebro dream --since 7 --apply
203
+ Mostre o relatório completo do que foi salvo.
204
+ """, encoding="utf-8")
205
+ print(f"[OK] Slash command criado: {dream_cmd}")
206
+
207
+ status_cmd = commands_dir / "cerebro-status.md"
208
+ if not status_cmd.exists():
209
+ status_cmd.write_text("""---
210
+ description: Ver status da memória do projeto
211
+ ---
212
+ Execute: ocerebro status
213
+ Liste quantas memórias existem por tipo.
214
+ """, encoding="utf-8")
215
+ print(f"[OK] Slash command criado: {status_cmd}")
216
+
217
+ gc_cmd = commands_dir / "cerebro-gc.md"
218
+ if not gc_cmd.exists():
219
+ gc_cmd.write_text("""---
220
+ description: Limpeza de memórias antigas
221
+ ---
222
+ Execute: ocerebro gc --threshold 30
223
+ Mostre o que será arquivado antes de confirmar.
224
+ """, encoding="utf-8")
225
+ print(f"[OK] Slash command criado: {gc_cmd}")
226
+
227
+ # Slash commands globais em ~/.claude/commands/
228
+ if global_commands:
229
+ global_commands_dir = Path.home() / ".claude" / "commands"
230
+ global_commands_dir.mkdir(parents=True, exist_ok=True)
231
+
232
+ dream_global = global_commands_dir / "cerebro-dream.md"
233
+ if not dream_global.exists():
234
+ dream_global.write_text("""---
235
+ description: Extrair memórias da sessão atual (global)
236
+ ---
237
+ Execute: ocerebro dream --since 7 --apply
238
+ Mostre o relatório completo do que foi salvo.
239
+ """, encoding="utf-8")
240
+ print(f"[OK] Slash command global criado: {dream_global}")
241
+
242
+ status_global = global_commands_dir / "cerebro-status.md"
243
+ if not status_global.exists():
244
+ status_global.write_text("""---
245
+ description: Ver status da memória (global)
246
+ ---
247
+ Execute: ocerebro status
248
+ Liste quantas memórias existem por tipo.
249
+ """, encoding="utf-8")
250
+ print(f"[OK] Slash command global criado: {status_global}")
251
+
252
+ gc_global = global_commands_dir / "cerebro-gc.md"
253
+ if not gc_global.exists():
254
+ gc_global.write_text("""---
255
+ description: Limpeza de memórias antigas (global)
256
+ ---
257
+ Execute: ocerebro gc --threshold 30
258
+ Mostre o que será arquivado antes de confirmar.
259
+ """, encoding="utf-8")
260
+ print(f"[OK] Slash command global criado: {gc_global}")
261
+
262
+ return True
263
+
264
+
265
+ def find_python_with_ocerebro() -> str:
266
+ """Encontra o executável Python onde ocerebro está instalado."""
267
+ candidates = []
268
+
269
+ try:
270
+ import ocerebro # noqa: F401
271
+ candidates.append(sys.executable)
272
+ except ImportError:
273
+ pass
274
+
275
+ for cmd in ["python", "python3"]:
276
+ try:
277
+ result = subprocess.run(
278
+ [cmd, "-c", "import ocerebro; print('ok')"],
279
+ capture_output=True,
280
+ text=True,
281
+ timeout=5
282
+ )
283
+ if result.returncode == 0:
284
+ result_path = subprocess.run(
285
+ [cmd, "-c", "import sys; print(sys.executable)"],
286
+ capture_output=True,
287
+ text=True
288
+ )
289
+ if result_path.returncode == 0:
290
+ candidates.append(result_path.stdout.strip())
291
+ except Exception:
292
+ continue
293
+
294
+ return candidates[0] if candidates else sys.executable
295
+
296
+
297
+ def get_claude_code_settings_path() -> Path | None:
298
+ """Encontra o settings.json do Claude Code."""
299
+ home_settings = Path.home() / ".claude" / "settings.json"
300
+ if home_settings.exists():
301
+ return home_settings
302
+
303
+ if sys.platform == "win32":
304
+ appdata = os.environ.get("APPDATA", "")
305
+ if appdata:
306
+ appdata_settings = Path(appdata) / "Claude" / "settings.json"
307
+ if appdata_settings.exists():
308
+ return appdata_settings
309
+
310
+ return home_settings
311
+
312
+
313
+ def get_claude_desktop_settings_path() -> Path | None:
314
+ """Encontra o claude_desktop.json do Claude Desktop."""
315
+ if sys.platform == "win32":
316
+ appdata = os.environ.get("APPDATA", "")
317
+ if appdata:
318
+ return Path(appdata) / "Claude" / "claude_desktop.json"
319
+ elif sys.platform == "darwin":
320
+ return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop.json"
321
+ return None
322
+
323
+
324
+ def setup_claude(auto: bool = True) -> bool:
325
+ """Configura MCP Server automaticamente."""
326
+ print("=" * 60)
327
+ print("OCerebro - Configurando MCP Server")
328
+ print("=" * 60)
329
+ print()
330
+
331
+ python_cmd = find_python_with_ocerebro()
332
+ print(f"[1/5] Python detectado: {python_cmd}")
333
+
334
+ # Encontra caminho do OCerebro
335
+ ocerebro_path = get_ocerebro_path()
336
+
337
+ # Gera configuração MCP
338
+ mcp_config = {
339
+ "command": python_cmd,
340
+ "args": ["-m", "src.mcp.server"],
341
+ "cwd": str(ocerebro_path),
342
+ "env": {}
343
+ }
344
+ print(f"[2/5] Configuração MCP gerada")
345
+
346
+ configured = []
347
+ errors = []
348
+
349
+ claude_code_path = get_claude_code_settings_path()
350
+ claude_desktop_path = get_claude_desktop_settings_path()
351
+
352
+ if auto:
353
+ targets = []
354
+ if claude_code_path and claude_code_path.exists():
355
+ targets.append(("code", claude_code_path))
356
+ if claude_desktop_path and claude_desktop_path.exists():
357
+ targets.append(("desktop", claude_desktop_path))
358
+ if not targets:
359
+ targets.append(("code", claude_code_path))
360
+ else:
361
+ print("Qual versão do Claude você usa?")
362
+ print(" 1. Claude Desktop")
363
+ print(" 2. Claude Code (claude.ai/code)")
364
+ print(" 3. Ambos")
365
+ choice = input("\nEscolha [1/2/3] (padrão: 2): ").strip() or "2"
366
+
367
+ targets = []
368
+ if choice in ["1", "3"] and claude_desktop_path:
369
+ targets.append(("desktop", claude_desktop_path))
370
+ if choice in ["2", "3"] and claude_code_path:
371
+ targets.append(("code", claude_code_path))
372
+ if not targets:
373
+ targets.append(("code", claude_code_path))
374
+
375
+ for target_type, config_path in targets:
376
+ print(f"[3/5] Configurando {target_type}...")
377
+
378
+ try:
379
+ config_path.parent.mkdir(parents=True, exist_ok=True)
380
+
381
+ existing_config = {}
382
+ if config_path.exists():
383
+ try:
384
+ existing_config = json.loads(config_path.read_text(encoding="utf-8"))
385
+ backup_config(config_path)
386
+ except json.JSONDecodeError:
387
+ print(f" Aviso: Config existente inválida, criando nova")
388
+
389
+ if "mcpServers" not in existing_config:
390
+ existing_config["mcpServers"] = {}
391
+ existing_config["mcpServers"]["ocerebro"] = mcp_config
392
+
393
+ if "mcp" not in existing_config:
394
+ existing_config["mcp"] = {}
395
+ existing_config["mcp"]["enabled"] = True
396
+
397
+ # Adiciona hook para dream automatico ao final da sessao
398
+ if "hooks" not in existing_config:
399
+ existing_config["hooks"] = {}
400
+
401
+ # Hook Stop: roda dream ao final de cada sessao
402
+ existing_config["hooks"]["Stop"] = [
403
+ {
404
+ "matcher": "",
405
+ "hooks": [
406
+ {
407
+ "type": "command",
408
+ "command": f"{python_cmd} -m src.cli.main dream --since 1 --apply --silent"
409
+ }
410
+ ]
411
+ }
412
+ ]
413
+
414
+ config_path.write_text(
415
+ json.dumps(existing_config, indent=2, ensure_ascii=False),
416
+ encoding="utf-8"
417
+ )
418
+
419
+ configured.append(target_type)
420
+ print(f" [OK] {target_type}: {config_path}")
421
+
422
+ except Exception as e:
423
+ errors.append(f"{target_type}: {e}")
424
+ print(f" [ERRO] {target_type}: {e}")
425
+
426
+ print()
427
+ print("=" * 60)
428
+ print("SETUP CONCLUÍDO!")
429
+ print("=" * 60)
430
+
431
+ if configured:
432
+ print(f"\n[OK] MCP Server configurado em: {', '.join(configured)}")
433
+ print("\nPróximos passos:")
434
+ print(" 1. Reinicie o Claude (feche e abra novamente)")
435
+ print(" 2. As ferramentas estarão disponíveis:")
436
+ for tool in ["cerebro_memory", "cerebro_search", "cerebro_checkpoint",
437
+ "cerebro_promote", "cerebro_status", "cerebro_hooks",
438
+ "cerebro_diff", "cerebro_dream", "cerebro_remember", "cerebro_gc"]:
439
+ print(f" - {tool}")
440
+ print("\nPara testar, digite no Claude:")
441
+ print(" /help (deve mostrar cerebro_*)")
442
+ print(" ou: Use cerebro_status")
443
+ else:
444
+ print("\n[ERRO] Não foi possível configurar automaticamente.")
445
+ print("Configure manualmente adicionando ao seu settings.json:")
446
+ print(json.dumps({"mcpServers": {"ocerebro": mcp_config}}, indent=2))
447
+
448
+ if errors:
449
+ print(f"\nErros encontrados: {len(errors)}")
450
+ for err in errors:
451
+ print(f" - {err}")
452
+
453
+ print()
454
+ return len(configured) > 0
455
+
456
+
457
+ def setup_hooks(project_path: Path | None = None) -> bool:
458
+ """Cria arquivo de exemplo hooks.yaml no projeto"""
459
+
460
+ if project_path is None:
461
+ project_path = Path.cwd()
462
+
463
+ hooks_yaml = project_path / "hooks.yaml"
464
+
465
+ if hooks_yaml.exists():
466
+ print(f" hooks.yaml já existe em {project_path}")
467
+ return False
468
+
469
+ example_config = """# OCerebro Hooks Configuration
470
+ hooks:
471
+ - name: error_notification
472
+ event_type: error
473
+ module_path: hooks/error_hook.py
474
+ function: on_error
475
+ config:
476
+ notify_severity: [\"critical\", \"high\"]
477
+
478
+ - name: llm_cost_tracker
479
+ event_type: tool_call
480
+ event_subtype: llm
481
+ module_path: hooks/cost_hook.py
482
+ function: on_llm_call
483
+ config:
484
+ monthly_budget: 100.0
485
+ alert_at_percentage: 80
486
+ """
487
+
488
+ hooks_yaml.write_text(example_config, encoding="utf-8")
489
+
490
+ hooks_dir = project_path / "hooks"
491
+ hooks_dir.mkdir(exist_ok=True)
492
+ (hooks_dir / "__init__.py").write_text('"""Hooks customizados do projeto"""', encoding="utf-8")
493
+
494
+ print(f"[OK] hooks.yaml criado em {project_path}")
495
+ print(f"[OK] Diretório hooks/ criado")
496
+
497
+ return True
498
+
499
+
500
+ def setup_ocerebro_dir(project_path: Path | None = None) -> bool:
501
+ """Cria diretório .ocerebro no projeto."""
502
+
503
+ if project_path is None:
504
+ project_path = Path.cwd()
505
+
506
+ project_path = project_path.resolve()
507
+ home = Path.home().resolve()
508
+ cwd = Path.cwd().resolve()
509
+
510
+ if not (str(project_path).startswith(str(home)) or
511
+ str(project_path).startswith(str(cwd))):
512
+ print(f"❌ Erro: path '{project_path}' fora do diretório permitido.")
513
+ return False
514
+
515
+ ocerebro_dir = project_path / ".ocerebro"
516
+
517
+ if ocerebro_dir.exists():
518
+ print(f"[OK] Diretório .ocerebro já existe")
519
+ return True
520
+
521
+ (ocerebro_dir / "raw").mkdir(parents=True)
522
+ (ocerebro_dir / "working").mkdir(parents=True)
523
+ (ocerebro_dir / "official").mkdir(parents=True)
524
+ (ocerebro_dir / "index").mkdir(parents=True)
525
+ (ocerebro_dir / "config").mkdir(parents=True)
526
+
527
+ gitignore = ocerebro_dir / ".gitignore"
528
+ gitignore.write_text("raw/\nworking/\nindex/\nconfig/local.yaml\n", encoding="utf-8")
529
+
530
+ print(f"[OK] Diretório .ocerebro criado em {project_path}")
531
+ print(f" - raw/ (eventos brutos)")
532
+ print(f" - working/ (rascunhos)")
533
+ print(f" - official/ (memória permanente)")
534
+ print(f" - index/ (banco de dados)")
535
+ print(f" - config/ (configurações)")
536
+
537
+ return True
538
+
539
+
540
+ def main():
541
+ """Função principal de setup"""
542
+
543
+ if len(sys.argv) > 1:
544
+ subcommand = sys.argv[1]
545
+
546
+ if subcommand == "claude":
547
+ success = setup_claude(auto=True)
548
+ sys.exit(0 if success else 1)
549
+
550
+ elif subcommand == "hooks":
551
+ project = Path(sys.argv[2]) if len(sys.argv) > 2 else None
552
+ success = setup_hooks(project)
553
+ sys.exit(0 if success else 1)
554
+
555
+ elif subcommand == "init":
556
+ project = Path(sys.argv[2]) if len(sys.argv) > 2 else Path.cwd()
557
+ setup_ocerebro_dir(project)
558
+ setup_hooks(project)
559
+ setup_slash_commands(project=project)
560
+ setup_claude(auto=True)
561
+ sys.exit(0)
562
+
563
+ else:
564
+ print(f"Subcomando desconhecido: {subcommand}")
565
+ sys.exit(1)
566
+
567
+ print("Executando setup completo...")
568
+ print()
569
+ setup_ocerebro_dir()
570
+ setup_hooks()
571
+ setup_claude(auto=True)
572
+
573
+
574
+ if __name__ == "__main__":
575
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocerebro",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "OCerebro - Sistema de Memoria para Agentes (Claude Code/MCP)",
5
5
  "main": "bin/ocerebro.js",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ocerebro"
7
- version = "0.1.8"
7
+ version = "0.2.0"
8
8
  description = "OCerebro - Sistema de Memoria para Agentes (Claude Code/MCP)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
package/src/cli/main.py CHANGED
@@ -363,6 +363,7 @@ def main():
363
363
  dream_parser = subparsers.add_parser("dream", help="Extração automática de memórias")
364
364
  dream_parser.add_argument("--since", type=int, default=7, dest="since_days")
365
365
  dream_parser.add_argument("--apply", action="store_true", dest="apply")
366
+ dream_parser.add_argument("--silent", action="store_true", dest="silent", help="Não imprimir output (para hooks)")
366
367
 
367
368
  # Comando: remember
368
369
  remember_parser = subparsers.add_parser("remember", help="Revisão e promoção de memórias")
@@ -434,6 +435,8 @@ def main():
434
435
  )
435
436
  elif args.command == "dream":
436
437
  result = cli.dream(since_days=args.since_days, dry_run=not args.apply)
438
+ if getattr(args, 'silent', False):
439
+ sys.exit(0)
437
440
  elif args.command == "remember":
438
441
  result = cli.remember(dry_run=not args.apply)
439
442
  elif args.command == "gc":
@@ -442,6 +445,8 @@ def main():
442
445
  parser.print_help()
443
446
  sys.exit(1)
444
447
 
448
+ if getattr(args, 'silent', False):
449
+ sys.exit(0)
445
450
  print(result)
446
451
 
447
452