claude-memory-agent 2.0.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/.env.example +107 -0
- package/README.md +200 -0
- package/agent_card.py +512 -0
- package/bin/cli.js +181 -0
- package/bin/postinstall.js +216 -0
- package/config.py +104 -0
- package/dashboard.html +2689 -0
- package/hooks/README.md +196 -0
- package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
- package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/hooks/auto-detect-response.py +348 -0
- package/hooks/auto_capture.py +255 -0
- package/hooks/detect-correction.py +173 -0
- package/hooks/grounding-hook.py +348 -0
- package/hooks/log-tool-use.py +234 -0
- package/hooks/log-user-request.py +208 -0
- package/hooks/pre-tool-decision.py +218 -0
- package/hooks/problem-detector.py +343 -0
- package/hooks/session_end.py +192 -0
- package/hooks/session_start.py +227 -0
- package/install.py +887 -0
- package/main.py +2859 -0
- package/manager.py +997 -0
- package/package.json +55 -0
- package/requirements.txt +8 -0
- package/run_server.py +136 -0
- package/services/__init__.py +50 -0
- package/services/__pycache__/__init__.cpython-312.pyc +0 -0
- package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
- package/services/__pycache__/auth.cpython-312.pyc +0 -0
- package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
- package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
- package/services/__pycache__/confidence.cpython-312.pyc +0 -0
- package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
- package/services/__pycache__/database.cpython-312.pyc +0 -0
- package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
- package/services/__pycache__/insights.cpython-312.pyc +0 -0
- package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
- package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
- package/services/__pycache__/timeline.cpython-312.pyc +0 -0
- package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
- package/services/__pycache__/websocket.cpython-312.pyc +0 -0
- package/services/agent_registry.py +753 -0
- package/services/auth.py +331 -0
- package/services/auto_inject.py +250 -0
- package/services/claude_md_sync.py +275 -0
- package/services/cleanup.py +667 -0
- package/services/compaction_flush.py +447 -0
- package/services/confidence.py +301 -0
- package/services/daily_log.py +333 -0
- package/services/database.py +2485 -0
- package/services/embeddings.py +358 -0
- package/services/insights.py +632 -0
- package/services/llm_analyzer.py +595 -0
- package/services/memory_md_sync.py +409 -0
- package/services/retry_queue.py +453 -0
- package/services/timeline.py +579 -0
- package/services/vector_index.py +398 -0
- package/services/websocket.py +257 -0
- package/skills/__init__.py +6 -0
- package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/__pycache__/admin.cpython-312.pyc +0 -0
- package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
- package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
- package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
- package/skills/__pycache__/insights.cpython-312.pyc +0 -0
- package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
- package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
- package/skills/__pycache__/search.cpython-312.pyc +0 -0
- package/skills/__pycache__/state.cpython-312.pyc +0 -0
- package/skills/__pycache__/store.cpython-312.pyc +0 -0
- package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
- package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
- package/skills/__pycache__/verification.cpython-312.pyc +0 -0
- package/skills/admin.py +469 -0
- package/skills/checkpoint.py +198 -0
- package/skills/claude_md.py +363 -0
- package/skills/cleanup.py +241 -0
- package/skills/grounding.py +801 -0
- package/skills/insights.py +231 -0
- package/skills/natural_language.py +277 -0
- package/skills/retrieve.py +67 -0
- package/skills/search.py +213 -0
- package/skills/state.py +182 -0
- package/skills/store.py +179 -0
- package/skills/summarize.py +588 -0
- package/skills/timeline.py +387 -0
- package/skills/verification.py +391 -0
- package/start_daemon.py +155 -0
- package/test_automation.py +221 -0
- package/test_complete.py +338 -0
- package/test_full.py +322 -0
- package/update_system.py +817 -0
- package/verify_db.py +134 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Auto-sync service for CLAUDE.md updates.
|
|
2
|
+
|
|
3
|
+
Automatically detects significant learnings and updates CLAUDE.md.
|
|
4
|
+
Runs periodically or triggered by insight detection.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, List, Optional
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Default CLAUDE.md locations
|
|
14
|
+
USER_CLAUDE_MD = Path.home() / ".claude" / "CLAUDE.md"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClaudeMdSync:
|
|
18
|
+
"""Syncs learned preferences and patterns to CLAUDE.md.
|
|
19
|
+
|
|
20
|
+
Features:
|
|
21
|
+
- Detects high-importance learnings
|
|
22
|
+
- Groups by category (preferences, patterns, rules)
|
|
23
|
+
- Avoids duplicates
|
|
24
|
+
- Preserves existing content
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, db, embeddings):
|
|
28
|
+
self.db = db
|
|
29
|
+
self.embeddings = embeddings
|
|
30
|
+
self._last_sync_time = 0
|
|
31
|
+
|
|
32
|
+
def _read_claude_md(self, path: Path) -> str:
|
|
33
|
+
"""Read current CLAUDE.md content."""
|
|
34
|
+
if path.exists():
|
|
35
|
+
return path.read_text(encoding='utf-8')
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
def _write_claude_md(self, path: Path, content: str):
|
|
39
|
+
"""Write content to CLAUDE.md."""
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
path.write_text(content, encoding='utf-8')
|
|
42
|
+
|
|
43
|
+
def _find_section(self, content: str, section_name: str) -> tuple[int, int]:
|
|
44
|
+
"""Find a section in CLAUDE.md by header name.
|
|
45
|
+
|
|
46
|
+
Returns (start_pos, end_pos) or (-1, -1) if not found.
|
|
47
|
+
"""
|
|
48
|
+
pattern = rf'^##\s+{re.escape(section_name)}\s*$'
|
|
49
|
+
lines = content.split('\n')
|
|
50
|
+
|
|
51
|
+
start_line = -1
|
|
52
|
+
for i, line in enumerate(lines):
|
|
53
|
+
if re.match(pattern, line, re.IGNORECASE):
|
|
54
|
+
start_line = i
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
if start_line == -1:
|
|
58
|
+
return -1, -1
|
|
59
|
+
|
|
60
|
+
# Find end (next ## header or end of file)
|
|
61
|
+
end_line = len(lines)
|
|
62
|
+
for i in range(start_line + 1, len(lines)):
|
|
63
|
+
if re.match(r'^##\s+', lines[i]):
|
|
64
|
+
end_line = i
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
# Convert to character positions
|
|
68
|
+
start_pos = sum(len(line) + 1 for line in lines[:start_line])
|
|
69
|
+
end_pos = sum(len(line) + 1 for line in lines[:end_line])
|
|
70
|
+
|
|
71
|
+
return start_pos, end_pos
|
|
72
|
+
|
|
73
|
+
def _content_exists(self, content: str, new_item: str) -> bool:
|
|
74
|
+
"""Check if similar content already exists in CLAUDE.md."""
|
|
75
|
+
# Normalize for comparison
|
|
76
|
+
new_normalized = re.sub(r'\s+', ' ', new_item.lower().strip())
|
|
77
|
+
if not new_normalized:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
for line in content.split('\n'):
|
|
81
|
+
line_normalized = re.sub(r'\s+', ' ', line.lower().strip())
|
|
82
|
+
# Skip empty lines
|
|
83
|
+
if not line_normalized:
|
|
84
|
+
continue
|
|
85
|
+
if new_normalized in line_normalized or line_normalized in new_normalized:
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
async def get_sync_candidates(
|
|
91
|
+
self,
|
|
92
|
+
project_path: Optional[str] = None,
|
|
93
|
+
min_importance: int = 7,
|
|
94
|
+
limit: int = 10
|
|
95
|
+
) -> List[Dict[str, Any]]:
|
|
96
|
+
"""Get memories that should be synced to CLAUDE.md.
|
|
97
|
+
|
|
98
|
+
Criteria:
|
|
99
|
+
- High importance (>= min_importance)
|
|
100
|
+
- Type is 'decision' or 'preference'
|
|
101
|
+
- Not already synced
|
|
102
|
+
"""
|
|
103
|
+
cursor = self.db.conn.cursor()
|
|
104
|
+
|
|
105
|
+
query = """
|
|
106
|
+
SELECT id, content, type, importance, tags, created_at
|
|
107
|
+
FROM memories
|
|
108
|
+
WHERE importance >= ?
|
|
109
|
+
AND type IN ('decision', 'preference')
|
|
110
|
+
AND (metadata IS NULL OR metadata NOT LIKE '%"synced_to_claude_md": true%')
|
|
111
|
+
"""
|
|
112
|
+
params = [min_importance]
|
|
113
|
+
|
|
114
|
+
if project_path:
|
|
115
|
+
query += " AND project_path = ?"
|
|
116
|
+
params.append(project_path)
|
|
117
|
+
|
|
118
|
+
query += " ORDER BY importance DESC, created_at DESC LIMIT ?"
|
|
119
|
+
params.append(limit)
|
|
120
|
+
|
|
121
|
+
cursor.execute(query, params)
|
|
122
|
+
rows = cursor.fetchall()
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
"id": row[0],
|
|
127
|
+
"content": row[1],
|
|
128
|
+
"type": row[2],
|
|
129
|
+
"importance": row[3],
|
|
130
|
+
"tags": row[4],
|
|
131
|
+
"created_at": row[5]
|
|
132
|
+
}
|
|
133
|
+
for row in rows
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
async def sync_to_claude_md(
|
|
137
|
+
self,
|
|
138
|
+
project_path: Optional[str] = None,
|
|
139
|
+
claude_md_path: Optional[Path] = None,
|
|
140
|
+
dry_run: bool = True
|
|
141
|
+
) -> Dict[str, Any]:
|
|
142
|
+
"""Sync high-importance learnings to CLAUDE.md.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
project_path: Filter to specific project
|
|
146
|
+
claude_md_path: Path to CLAUDE.md (default: user's global)
|
|
147
|
+
dry_run: If True, only preview what would be synced
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Sync results
|
|
151
|
+
"""
|
|
152
|
+
path = claude_md_path or USER_CLAUDE_MD
|
|
153
|
+
candidates = await self.get_sync_candidates(project_path)
|
|
154
|
+
|
|
155
|
+
if not candidates:
|
|
156
|
+
return {
|
|
157
|
+
"success": True,
|
|
158
|
+
"synced": 0,
|
|
159
|
+
"message": "No new learnings to sync"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
current_content = self._read_claude_md(path)
|
|
163
|
+
additions = []
|
|
164
|
+
|
|
165
|
+
for candidate in candidates:
|
|
166
|
+
# Check if already exists
|
|
167
|
+
if self._content_exists(current_content, candidate["content"]):
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
# Categorize
|
|
171
|
+
if candidate["type"] == "preference":
|
|
172
|
+
section = "Preferences"
|
|
173
|
+
elif "pattern" in (candidate.get("tags") or ""):
|
|
174
|
+
section = "Learned Patterns"
|
|
175
|
+
else:
|
|
176
|
+
section = "Project-Specific Rules"
|
|
177
|
+
|
|
178
|
+
additions.append({
|
|
179
|
+
"section": section,
|
|
180
|
+
"content": f"- {candidate['content'][:200]}",
|
|
181
|
+
"memory_id": candidate["id"],
|
|
182
|
+
"importance": candidate["importance"]
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
if dry_run:
|
|
186
|
+
return {
|
|
187
|
+
"success": True,
|
|
188
|
+
"dry_run": True,
|
|
189
|
+
"would_sync": len(additions),
|
|
190
|
+
"additions": additions
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Actually sync
|
|
194
|
+
synced_ids = []
|
|
195
|
+
for addition in additions:
|
|
196
|
+
section = addition["section"]
|
|
197
|
+
new_line = addition["content"]
|
|
198
|
+
|
|
199
|
+
# Find or create section
|
|
200
|
+
start, end = self._find_section(current_content, section)
|
|
201
|
+
|
|
202
|
+
if start == -1:
|
|
203
|
+
# Create new section at end
|
|
204
|
+
current_content += f"\n\n## {section}\n{new_line}\n"
|
|
205
|
+
else:
|
|
206
|
+
# Insert into existing section
|
|
207
|
+
section_content = current_content[start:end]
|
|
208
|
+
# Insert before end of section
|
|
209
|
+
insert_pos = end - 1 if end > 0 else len(current_content)
|
|
210
|
+
current_content = (
|
|
211
|
+
current_content[:insert_pos] +
|
|
212
|
+
f"\n{new_line}" +
|
|
213
|
+
current_content[insert_pos:]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
synced_ids.append(addition["memory_id"])
|
|
217
|
+
|
|
218
|
+
# Write updated content
|
|
219
|
+
self._write_claude_md(path, current_content)
|
|
220
|
+
|
|
221
|
+
# Mark memories as synced
|
|
222
|
+
cursor = self.db.conn.cursor()
|
|
223
|
+
for mem_id in synced_ids:
|
|
224
|
+
cursor.execute("""
|
|
225
|
+
UPDATE memories
|
|
226
|
+
SET metadata = COALESCE(metadata, '{}')
|
|
227
|
+
WHERE id = ?
|
|
228
|
+
""", [mem_id])
|
|
229
|
+
# TODO: Properly update JSON metadata
|
|
230
|
+
|
|
231
|
+
self.db.conn.commit()
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
"success": True,
|
|
235
|
+
"synced": len(synced_ids),
|
|
236
|
+
"synced_ids": synced_ids,
|
|
237
|
+
"path": str(path)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async def suggest_updates(
|
|
241
|
+
self,
|
|
242
|
+
project_path: Optional[str] = None
|
|
243
|
+
) -> Dict[str, Any]:
|
|
244
|
+
"""Get suggestions for CLAUDE.md updates without syncing.
|
|
245
|
+
|
|
246
|
+
Returns formatted suggestions that can be reviewed.
|
|
247
|
+
"""
|
|
248
|
+
candidates = await self.get_sync_candidates(project_path, min_importance=6)
|
|
249
|
+
|
|
250
|
+
suggestions = []
|
|
251
|
+
for c in candidates:
|
|
252
|
+
suggestions.append({
|
|
253
|
+
"type": c["type"],
|
|
254
|
+
"content": c["content"],
|
|
255
|
+
"importance": c["importance"],
|
|
256
|
+
"suggestion": f"Add to CLAUDE.md: {c['content'][:150]}..."
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"success": True,
|
|
261
|
+
"suggestions": suggestions,
|
|
262
|
+
"count": len(suggestions)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# Global instance
|
|
267
|
+
_sync_service: Optional[ClaudeMdSync] = None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_claude_md_sync(db, embeddings) -> ClaudeMdSync:
|
|
271
|
+
"""Get the global CLAUDE.md sync service."""
|
|
272
|
+
global _sync_service
|
|
273
|
+
if _sync_service is None:
|
|
274
|
+
_sync_service = ClaudeMdSync(db, embeddings)
|
|
275
|
+
return _sync_service
|