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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -0
  3. package/bin/ocerebro.js +98 -0
  4. package/cerebro/__init__.py +7 -0
  5. package/cerebro/__main__.py +19 -0
  6. package/cerebro/cerebro_setup.py +459 -0
  7. package/hooks/__init__.py +1 -0
  8. package/hooks/cost_hook.py +45 -0
  9. package/hooks/coverage_hook.py +39 -0
  10. package/hooks/error_hook.py +48 -0
  11. package/hooks/expensive_hook.py +41 -0
  12. package/hooks/global_logger.py +32 -0
  13. package/package.json +49 -0
  14. package/pyproject.toml +77 -0
  15. package/src/__init__.py +2 -0
  16. package/src/cli/__init__.py +2 -0
  17. package/src/cli/dream.py +91 -0
  18. package/src/cli/gc.py +93 -0
  19. package/src/cli/main.py +583 -0
  20. package/src/cli/remember.py +74 -0
  21. package/src/consolidation/__init__.py +8 -0
  22. package/src/consolidation/checkpoints.py +96 -0
  23. package/src/consolidation/dream.py +465 -0
  24. package/src/consolidation/extractor.py +313 -0
  25. package/src/consolidation/promoter.py +435 -0
  26. package/src/consolidation/remember.py +544 -0
  27. package/src/consolidation/scorer.py +191 -0
  28. package/src/core/__init__.py +6 -0
  29. package/src/core/event_schema.py +55 -0
  30. package/src/core/jsonl_storage.py +238 -0
  31. package/src/core/paths.py +254 -0
  32. package/src/core/session_manager.py +76 -0
  33. package/src/diff/__init__.py +5 -0
  34. package/src/diff/memory_diff.py +571 -0
  35. package/src/forgetting/__init__.py +6 -0
  36. package/src/forgetting/decay.py +86 -0
  37. package/src/forgetting/gc.py +296 -0
  38. package/src/forgetting/guard_rails.py +126 -0
  39. package/src/hooks/__init__.py +11 -0
  40. package/src/hooks/core_captures.py +170 -0
  41. package/src/hooks/custom_loader.py +389 -0
  42. package/src/index/__init__.py +7 -0
  43. package/src/index/embeddings_db.py +419 -0
  44. package/src/index/metadata_db.py +230 -0
  45. package/src/index/queries.py +357 -0
  46. package/src/mcp/__init__.py +2 -0
  47. package/src/mcp/server.py +640 -0
  48. package/src/memdir/__init__.py +19 -0
  49. package/src/memdir/scanner.py +260 -0
  50. package/src/official/__init__.py +5 -0
  51. package/src/official/markdown_storage.py +173 -0
  52. package/src/official/templates.py +128 -0
  53. package/src/working/__init__.py +5 -0
  54. package/src/working/memory_view.py +150 -0
  55. 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
+ }