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.
Files changed (100) hide show
  1. package/.env.example +107 -0
  2. package/README.md +200 -0
  3. package/agent_card.py +512 -0
  4. package/bin/cli.js +181 -0
  5. package/bin/postinstall.js +216 -0
  6. package/config.py +104 -0
  7. package/dashboard.html +2689 -0
  8. package/hooks/README.md +196 -0
  9. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  10. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  11. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  12. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  13. package/hooks/auto-detect-response.py +348 -0
  14. package/hooks/auto_capture.py +255 -0
  15. package/hooks/detect-correction.py +173 -0
  16. package/hooks/grounding-hook.py +348 -0
  17. package/hooks/log-tool-use.py +234 -0
  18. package/hooks/log-user-request.py +208 -0
  19. package/hooks/pre-tool-decision.py +218 -0
  20. package/hooks/problem-detector.py +343 -0
  21. package/hooks/session_end.py +192 -0
  22. package/hooks/session_start.py +227 -0
  23. package/install.py +887 -0
  24. package/main.py +2859 -0
  25. package/manager.py +997 -0
  26. package/package.json +55 -0
  27. package/requirements.txt +8 -0
  28. package/run_server.py +136 -0
  29. package/services/__init__.py +50 -0
  30. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  31. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  32. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  33. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  34. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  35. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  36. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  37. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  38. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  39. package/services/__pycache__/database.cpython-312.pyc +0 -0
  40. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  41. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  42. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  43. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  44. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  45. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  46. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  47. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  48. package/services/agent_registry.py +753 -0
  49. package/services/auth.py +331 -0
  50. package/services/auto_inject.py +250 -0
  51. package/services/claude_md_sync.py +275 -0
  52. package/services/cleanup.py +667 -0
  53. package/services/compaction_flush.py +447 -0
  54. package/services/confidence.py +301 -0
  55. package/services/daily_log.py +333 -0
  56. package/services/database.py +2485 -0
  57. package/services/embeddings.py +358 -0
  58. package/services/insights.py +632 -0
  59. package/services/llm_analyzer.py +595 -0
  60. package/services/memory_md_sync.py +409 -0
  61. package/services/retry_queue.py +453 -0
  62. package/services/timeline.py +579 -0
  63. package/services/vector_index.py +398 -0
  64. package/services/websocket.py +257 -0
  65. package/skills/__init__.py +6 -0
  66. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  67. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  68. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  69. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  70. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  71. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  72. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  73. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  74. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  75. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  76. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  77. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  78. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  81. package/skills/admin.py +469 -0
  82. package/skills/checkpoint.py +198 -0
  83. package/skills/claude_md.py +363 -0
  84. package/skills/cleanup.py +241 -0
  85. package/skills/grounding.py +801 -0
  86. package/skills/insights.py +231 -0
  87. package/skills/natural_language.py +277 -0
  88. package/skills/retrieve.py +67 -0
  89. package/skills/search.py +213 -0
  90. package/skills/state.py +182 -0
  91. package/skills/store.py +179 -0
  92. package/skills/summarize.py +588 -0
  93. package/skills/timeline.py +387 -0
  94. package/skills/verification.py +391 -0
  95. package/start_daemon.py +155 -0
  96. package/test_automation.py +221 -0
  97. package/test_complete.py +338 -0
  98. package/test_full.py +322 -0
  99. package/update_system.py +817 -0
  100. 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