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,254 @@
1
+ """Resolução de paths do sistema de memória do Claude Code.
2
+
3
+ Replica exatamente a lógica do Claude Code:
4
+ - src/memdir/paths.ts — sanitizePath(), getAutoMemPath(), getAutoMemDailyLogPath()
5
+ - CLAUDE_COWORK_MEMORY_PATH_OVERRIDE (env var) tem prioridade
6
+ - autoMemoryDirectory em settings.json como fallback
7
+ - ~/.claude/projects/<sanitized-git-root>/memory/ como default
8
+ """
9
+
10
+ import os
11
+ import re
12
+ from datetime import date
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+
17
+ def sanitize_path(absolute_path: str) -> str:
18
+ """
19
+ Replica sanitizePath() do Claude Code (src/memdir/paths.ts).
20
+
21
+ Converte separadores de path em '-' para formar o nome do diretório.
22
+ Remove caracteres especiais e normaliza para ASCII seguro.
23
+
24
+ Exemplo:
25
+ /home/user/projects/ocerebro → -home-user-projects-ocerebro
26
+ C:\\Users\\dev\\my-project → C--Users-dev-my-project
27
+
28
+ Args:
29
+ absolute_path: Path absoluto para sanitizar
30
+
31
+ Returns:
32
+ String sanitizada para uso como nome de diretório
33
+ """
34
+ # Normaliza separadores Windows para Unix
35
+ normalized = absolute_path.replace("\\", "/")
36
+
37
+ # Substitui separadores de path por '-'
38
+ sanitized = re.sub(r'[/\\]', '-', normalized)
39
+
40
+ # Remove caracteres especiais (mantém apenas alfanuméricos, '-', '_')
41
+ sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', sanitized)
42
+
43
+ # Remove múltiplos '-' consecutivos
44
+ sanitized = re.sub(r'-+', '-', sanitized)
45
+
46
+ # Garante que começa com '-' se o path original era absoluto
47
+ if absolute_path.startswith('/') or absolute_path.startswith('\\'):
48
+ if not sanitized.startswith('-'):
49
+ sanitized = '-' + sanitized
50
+ elif len(absolute_path) >= 2 and absolute_path[1] == ':':
51
+ # Windows path (ex: C:\)
52
+ if not sanitized.startswith('-'):
53
+ sanitized = '-' + sanitized
54
+
55
+ return sanitized
56
+
57
+
58
+ def get_git_root(project_root: Path = None) -> Path:
59
+ """
60
+ Encontra a raiz do repositório git.
61
+
62
+ Args:
63
+ project_root: Diretório inicial para busca (default: cwd)
64
+
65
+ Returns:
66
+ Path da raiz do git
67
+
68
+ Raises:
69
+ FileNotFoundError: Se não estiver em repositório git
70
+ """
71
+ if project_root is None:
72
+ project_root = Path.cwd()
73
+
74
+ current = project_root.resolve()
75
+
76
+ while current != current.parent:
77
+ if (current / ".git").exists():
78
+ return current
79
+ current = current.parent
80
+
81
+ raise FileNotFoundError(
82
+ f"Não foi possível encontrar raiz do git a partir de {project_root}"
83
+ )
84
+
85
+
86
+ def get_claude_home() -> Path:
87
+ """
88
+ Retorna o diretório base do Claude (~/.claude).
89
+
90
+ Priority:
91
+ 1. CLAUDE_HOME env var
92
+ 2. ~/.claude (default)
93
+
94
+ Returns:
95
+ Path do diretório Claude home
96
+ """
97
+ claude_home = os.environ.get("CLAUDE_HOME")
98
+ if claude_home:
99
+ return Path(claude_home).resolve()
100
+
101
+ # Default: ~/.claude
102
+ home = Path.home()
103
+ return home / ".claude"
104
+
105
+
106
+ def get_auto_mem_path(project_root: Path = None) -> Path:
107
+ """
108
+ Resolve o path do diretório de memória automática.
109
+
110
+ Priority:
111
+ 1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE (env var)
112
+ 2. autoMemoryDirectory em ~/.claude/settings.json
113
+ 3. ~/.claude/projects/<sanitized-git-root>/memory/
114
+
115
+ Replica:
116
+ - getAutoMemPath() do Claude Code
117
+ - Priority: env override > settings.json > default
118
+
119
+ Args:
120
+ project_root: Diretório do projeto (default: git root do cwd)
121
+
122
+ Returns:
123
+ Path do diretório de memória
124
+ """
125
+ # Priority 1: Environment variable override
126
+ override = os.environ.get("CLAUDE_COWORK_MEMORY_PATH_OVERRIDE")
127
+ if override:
128
+ mem_path = Path(override).resolve()
129
+ mem_path.mkdir(parents=True, exist_ok=True)
130
+ return mem_path
131
+
132
+ # Priority 2: Check settings.json (TODO: implementar quando settings.json parser existir)
133
+ # settings_path = get_claude_home() / "settings.json"
134
+ # if settings_path.exists():
135
+ # settings = json.loads(settings_path.read_text())
136
+ # if "autoMemoryDirectory" in settings:
137
+ # return Path(settings["autoMemoryDirectory"]).resolve()
138
+
139
+ # Priority 3: Default path based on git root
140
+ try:
141
+ git_root = get_git_root(project_root)
142
+ sanitized = sanitize_path(str(git_root))
143
+ except FileNotFoundError:
144
+ # Fallback: usa cwd se não tiver git
145
+ sanitized = sanitize_path(str(Path.cwd().resolve()))
146
+
147
+ claude_home = get_claude_home()
148
+ mem_dir = claude_home / "projects" / sanitized / "memory"
149
+ mem_dir.mkdir(parents=True, exist_ok=True)
150
+
151
+ return mem_dir
152
+
153
+
154
+ def get_memory_index(memory_dir: Path = None) -> Path:
155
+ """
156
+ Retorna o path do arquivo MEMORY.md (índice de memórias).
157
+
158
+ Replica: getMemoryIndexPath() do Claude Code
159
+
160
+ Args:
161
+ memory_dir: Diretório de memória (default: get_auto_mem_path())
162
+
163
+ Returns:
164
+ Path para MEMORY.md
165
+ """
166
+ if memory_dir is None:
167
+ memory_dir = get_auto_mem_path()
168
+
169
+ return memory_dir / "MEMORY.md"
170
+
171
+
172
+ def get_daily_log_path(memory_dir: Path = None, date_: date = None) -> Path:
173
+ """
174
+ Retorna o path do log diário (KAIROS).
175
+
176
+ Replica: getAutoMemDailyLogPath() do Claude Code
177
+ Path: memory_dir / logs / YYYY / MM / YYYY-MM-DD.md
178
+
179
+ Args:
180
+ memory_dir: Diretório de memória (default: get_auto_mem_path())
181
+ date_: Data do log (default: hoje)
182
+
183
+ Returns:
184
+ Path para o arquivo de log diário
185
+ """
186
+ if memory_dir is None:
187
+ memory_dir = get_auto_mem_path()
188
+
189
+ if date_ is None:
190
+ date_ = date.today()
191
+
192
+ # Estrutura: logs/YYYY/MM/YYYY-MM-DD.md
193
+ year = date_.year
194
+ month = date_.month
195
+ day_file = f"{date_.isoformat()}.md"
196
+
197
+ log_dir = memory_dir / "logs" / str(year) / f"{month:02d}"
198
+ log_dir.mkdir(parents=True, exist_ok=True)
199
+
200
+ return log_dir / day_file
201
+
202
+
203
+ def get_user_memory_path(memory_dir: Path = None) -> Path:
204
+ """
205
+ Retorna o path preferencial para CLAUDE.local.md (memória do usuário).
206
+
207
+ Args:
208
+ memory_dir: Diretório de memória (default: get_auto_mem_path())
209
+
210
+ Returns:
211
+ Path para CLAUDE.local.md
212
+ """
213
+ if memory_dir is None:
214
+ memory_dir = get_auto_mem_path()
215
+
216
+ # Nota: CLAUDE.local.md fica na raiz do projeto, não em memory_dir
217
+ # Este método é um placeholder para futura integração
218
+ try:
219
+ git_root = get_git_root()
220
+ return git_root / "CLAUDE.local.md"
221
+ except FileNotFoundError:
222
+ # Fallback: memory_dir
223
+ return memory_dir / "CLAUDE.local.md"
224
+
225
+
226
+ def get_project_memory_path(memory_dir: Path = None) -> Path:
227
+ """
228
+ Retorna o path preferencial para CLAUDE.md (memória do projeto).
229
+
230
+ Args:
231
+ memory_dir: Diretório de memória (default: get_auto_mem_path())
232
+
233
+ Returns:
234
+ Path para CLAUDE.md
235
+ """
236
+ if memory_dir is None:
237
+ memory_dir = get_auto_mem_path()
238
+
239
+ # Nota: CLAUDE.md fica na raiz do projeto, não em memory_dir
240
+ # Este método é um placeholder para futura integração
241
+ try:
242
+ git_root = get_git_root()
243
+ return git_root / "CLAUDE.md"
244
+ except FileNotFoundError:
245
+ # Fallback: memory_dir
246
+ return memory_dir / "CLAUDE.md"
247
+
248
+
249
+ # Constants - replica de memoryScan.ts
250
+ MAX_MEMORY_FILES = 200
251
+ FRONTMATTER_MAX_LINES = 30
252
+ MEMORY_INDEX_FILENAME = "MEMORY.md"
253
+ MEMORY_INDEX_MAX_LINES = 200
254
+ MEMORY_INDEX_MAX_SIZE_KB = 25
@@ -0,0 +1,76 @@
1
+ """Gerenciamento de sessão e detecção de projeto"""
2
+
3
+ import uuid
4
+ import yaml
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+
9
+ class SessionManager:
10
+ """
11
+ Gerencia session ID e detecção de projeto.
12
+
13
+ Session ID é persistido em .cerebro_session para reutilização
14
+ entre reinícios da sessão.
15
+
16
+ Detecção de projeto usa cerebro-project.yaml ou fallback para
17
+ nome do diretório.
18
+ """
19
+
20
+ def __init__(self, cerebro_path: Path):
21
+ """
22
+ Inicializa o SessionManager.
23
+
24
+ Args:
25
+ cerebro_path: Diretório base do Cerebro
26
+ """
27
+ self.cerebro_path = cerebro_path
28
+ self._session_file = cerebro_path / ".cerebro_session"
29
+
30
+ def get_session_id(self) -> str:
31
+ """
32
+ Obtém ou cria um session ID.
33
+
34
+ Reusa session ID existente se disponível, caso contrário
35
+ cria um novo e persiste em .cerebro_session.
36
+
37
+ Returns:
38
+ Session ID no formato sess_XXXXXXXX
39
+ """
40
+ if self._session_file.exists():
41
+ return self._session_file.read_text().strip()
42
+
43
+ session_id = f"sess_{uuid.uuid4().hex[:8]}"
44
+ self._session_file.write_text(session_id)
45
+ return session_id
46
+
47
+ def detect_project(self, project_dir: Path) -> str:
48
+ """
49
+ Detecta o ID do projeto a partir do diretório.
50
+
51
+ Prioridade:
52
+ 1. project_id em .claude/cerebro-project.yaml
53
+ 2. Nome do diretório do projeto
54
+
55
+ Args:
56
+ project_dir: Diretório do projeto
57
+
58
+ Returns:
59
+ ID do projeto
60
+ """
61
+ cerebro_yaml = project_dir / ".claude" / "cerebro-project.yaml"
62
+
63
+ if cerebro_yaml.exists():
64
+ config = yaml.safe_load(cerebro_yaml.read_text())
65
+ return config.get("project_id", project_dir.name)
66
+
67
+ return project_dir.name
68
+
69
+ def clear_session(self) -> None:
70
+ """
71
+ Limpa o session ID (fim de sessão).
72
+
73
+ Remove o arquivo .cerebro_session.
74
+ """
75
+ if self._session_file.exists():
76
+ self._session_file.unlink()
@@ -0,0 +1,5 @@
1
+ """Memory Diff: análise diferencial de memória entre dois pontos no tempo"""
2
+
3
+ from src.diff.memory_diff import MemoryDiff, MemoryDiffResult
4
+
5
+ __all__ = ["MemoryDiff", "MemoryDiffResult"]