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,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()
|