claude-memory-agent 2.1.0 → 2.2.1
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/bin/cli.js +11 -1
- package/bin/lib/banner.js +39 -0
- package/bin/lib/environment.js +166 -0
- package/bin/lib/installer.js +291 -0
- package/bin/lib/models.js +95 -0
- package/bin/lib/steps/advanced.js +101 -0
- package/bin/lib/steps/confirm.js +87 -0
- package/bin/lib/steps/model.js +57 -0
- package/bin/lib/steps/provider.js +65 -0
- package/bin/lib/steps/scope.js +59 -0
- package/bin/lib/steps/server.js +74 -0
- package/bin/lib/ui.js +75 -0
- package/bin/onboarding.js +164 -0
- package/bin/postinstall.js +22 -257
- package/config.py +103 -4
- package/dashboard.html +697 -27
- package/hooks/extract_memories.py +439 -0
- package/hooks/pre_compact_hook.py +76 -0
- package/hooks/session_end_hook.py +149 -0
- package/hooks/stop_hook.py +372 -0
- package/install.py +91 -37
- package/main.py +1636 -892
- package/mcp_server.py +451 -0
- package/package.json +14 -3
- package/requirements.txt +12 -8
- package/services/adaptive_ranker.py +272 -0
- package/services/agent_catalog.json +153 -0
- package/services/agent_registry.py +245 -730
- package/services/claude_md_sync.py +320 -4
- package/services/consolidation.py +417 -0
- package/services/database.py +586 -105
- package/services/embedding_pipeline.py +262 -0
- package/services/embeddings.py +493 -85
- package/services/memory_decay.py +408 -0
- package/services/native_memory_paths.py +86 -0
- package/services/native_memory_sync.py +496 -0
- package/services/response_manager.py +183 -0
- package/services/terminal_ui.py +199 -0
- package/services/tier_manager.py +235 -0
- package/services/websocket.py +26 -6
- package/skills/search.py +136 -61
- package/skills/session_review.py +210 -23
- package/skills/store.py +125 -18
- package/terminal_dashboard.py +474 -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__/grounding-hook.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/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__/curator.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/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__/confidence_tracker.cpython-312.pyc +0 -0
- package/skills/__pycache__/context.cpython-312.pyc +0 -0
- package/skills/__pycache__/curator.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__/session_review.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/test_automation.py +0 -221
- package/test_complete.py +0 -338
- package/test_full.py +0 -322
- package/verify_db.py +0 -134
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Automatically detects significant learnings and updates CLAUDE.md.
|
|
4
4
|
Runs periodically or triggered by insight detection.
|
|
5
|
+
|
|
6
|
+
Tier 1 auto-generation: Writes the top-ranked memories directly into CLAUDE.md
|
|
7
|
+
so Claude reads them at session start with zero API cost. The auto-generated
|
|
8
|
+
section is delimited by HTML comment markers and is replaced on each run.
|
|
5
9
|
"""
|
|
6
10
|
import os
|
|
7
11
|
import re
|
|
12
|
+
import logging
|
|
8
13
|
from pathlib import Path
|
|
9
14
|
from typing import Dict, Any, List, Optional
|
|
10
15
|
from datetime import datetime
|
|
11
16
|
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
12
19
|
|
|
13
20
|
# Default CLAUDE.md locations
|
|
14
21
|
USER_CLAUDE_MD = Path.home() / ".claude" / "CLAUDE.md"
|
|
@@ -27,7 +34,6 @@ class ClaudeMdSync:
|
|
|
27
34
|
def __init__(self, db, embeddings):
|
|
28
35
|
self.db = db
|
|
29
36
|
self.embeddings = embeddings
|
|
30
|
-
self._last_sync_time = 0
|
|
31
37
|
|
|
32
38
|
def _read_claude_md(self, path: Path) -> str:
|
|
33
39
|
"""Read current CLAUDE.md content."""
|
|
@@ -218,15 +224,14 @@ class ClaudeMdSync:
|
|
|
218
224
|
# Write updated content
|
|
219
225
|
self._write_claude_md(path, current_content)
|
|
220
226
|
|
|
221
|
-
# Mark memories as synced
|
|
227
|
+
# Mark memories as synced by setting synced_to_claude_md flag in metadata JSON
|
|
222
228
|
cursor = self.db.conn.cursor()
|
|
223
229
|
for mem_id in synced_ids:
|
|
224
230
|
cursor.execute("""
|
|
225
231
|
UPDATE memories
|
|
226
|
-
SET metadata = COALESCE(metadata, '{}')
|
|
232
|
+
SET metadata = json_set(COALESCE(metadata, '{}'), '$.synced_to_claude_md', json('true'))
|
|
227
233
|
WHERE id = ?
|
|
228
234
|
""", [mem_id])
|
|
229
|
-
# TODO: Properly update JSON metadata
|
|
230
235
|
|
|
231
236
|
self.db.conn.commit()
|
|
232
237
|
|
|
@@ -237,6 +242,317 @@ class ClaudeMdSync:
|
|
|
237
242
|
"path": str(path)
|
|
238
243
|
}
|
|
239
244
|
|
|
245
|
+
# ================================================================
|
|
246
|
+
# TIER 1 AUTO-GENERATION - Zero-cost startup context
|
|
247
|
+
# ================================================================
|
|
248
|
+
|
|
249
|
+
# Markers that delimit the auto-generated section in CLAUDE.md.
|
|
250
|
+
# Everything between these markers is replaced on each run.
|
|
251
|
+
TIER1_START_MARKER = "<!-- AUTO-MEMORY START -->"
|
|
252
|
+
TIER1_END_MARKER = "<!-- AUTO-MEMORY END -->"
|
|
253
|
+
|
|
254
|
+
# Maximum output budget
|
|
255
|
+
TIER1_MAX_LINES = 150
|
|
256
|
+
TIER1_MAX_CHARS = 4000
|
|
257
|
+
TIER1_CONTENT_TRUNCATE = 100 # Truncate individual content strings
|
|
258
|
+
|
|
259
|
+
# Category mapping from memory type to output section header
|
|
260
|
+
TIER1_CATEGORIES = {
|
|
261
|
+
"decision": "Decisions",
|
|
262
|
+
"preference": "Preferences",
|
|
263
|
+
"error": "Known Issues",
|
|
264
|
+
"code": "Patterns",
|
|
265
|
+
"chunk": "Patterns",
|
|
266
|
+
"session": "Patterns",
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async def auto_generate_tier1(
|
|
270
|
+
self,
|
|
271
|
+
project_path: Optional[str] = None,
|
|
272
|
+
max_memories: int = 30
|
|
273
|
+
) -> Dict[str, Any]:
|
|
274
|
+
"""Generate Tier 1 context from the top-ranked memories.
|
|
275
|
+
|
|
276
|
+
Queries the database for memories ranked by a composite score:
|
|
277
|
+
score = (importance * 0.4) + (confidence * 0.3)
|
|
278
|
+
+ (access_frequency * 0.2) + (recency * 0.1)
|
|
279
|
+
|
|
280
|
+
Where:
|
|
281
|
+
access_frequency = min(access_count / 10, 1.0)
|
|
282
|
+
recency = max(0, 1 - age_days / 90)
|
|
283
|
+
|
|
284
|
+
Includes ALL memory types, not just decision/preference.
|
|
285
|
+
Formats the output as categorized markdown within ~150 lines.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
project_path: Optional filter to a specific project
|
|
289
|
+
max_memories: Maximum number of memories to include
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Dict with 'success', 'markdown', 'memory_count', 'categories'
|
|
293
|
+
"""
|
|
294
|
+
cursor = self.db.conn.cursor()
|
|
295
|
+
|
|
296
|
+
# Build query -- compute the composite score in SQL
|
|
297
|
+
# SQLite does not have DATEDIFF, so we compute age via julianday.
|
|
298
|
+
query = """
|
|
299
|
+
SELECT
|
|
300
|
+
id,
|
|
301
|
+
type,
|
|
302
|
+
content,
|
|
303
|
+
importance,
|
|
304
|
+
confidence,
|
|
305
|
+
access_count,
|
|
306
|
+
created_at,
|
|
307
|
+
outcome,
|
|
308
|
+
success,
|
|
309
|
+
project_path,
|
|
310
|
+
project_name,
|
|
311
|
+
(
|
|
312
|
+
(COALESCE(importance, 5) / 10.0) * 0.4
|
|
313
|
+
+ COALESCE(confidence, 0.5) * 0.3
|
|
314
|
+
+ MIN(COALESCE(access_count, 0) / 10.0, 1.0) * 0.2
|
|
315
|
+
+ MAX(0.0, 1.0 - (julianday('now') - julianday(COALESCE(created_at, datetime('now')))) / 90.0) * 0.1
|
|
316
|
+
) AS tier1_score
|
|
317
|
+
FROM memories
|
|
318
|
+
WHERE importance >= 5
|
|
319
|
+
AND COALESCE(outcome_status, 'pending') != 'failed'
|
|
320
|
+
AND COALESCE(failure_count, 0) < 3
|
|
321
|
+
"""
|
|
322
|
+
params: List[Any] = []
|
|
323
|
+
|
|
324
|
+
if project_path:
|
|
325
|
+
query += " AND project_path = ?"
|
|
326
|
+
params.append(project_path)
|
|
327
|
+
|
|
328
|
+
query += " ORDER BY tier1_score DESC LIMIT ?"
|
|
329
|
+
params.append(max_memories)
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
cursor.execute(query, params)
|
|
333
|
+
rows = cursor.fetchall()
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.error(f"Tier 1 query failed: {e}")
|
|
336
|
+
return {
|
|
337
|
+
"success": False,
|
|
338
|
+
"error": str(e),
|
|
339
|
+
"markdown": "",
|
|
340
|
+
"memory_count": 0,
|
|
341
|
+
"categories": {}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if not rows:
|
|
345
|
+
empty_md = self._format_tier1_section({})
|
|
346
|
+
return {
|
|
347
|
+
"success": True,
|
|
348
|
+
"markdown": empty_md,
|
|
349
|
+
"memory_count": 0,
|
|
350
|
+
"categories": {}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# Group rows by category
|
|
354
|
+
categorized: Dict[str, List[Dict[str, Any]]] = {}
|
|
355
|
+
for row in rows:
|
|
356
|
+
row_dict = dict(row)
|
|
357
|
+
mem_type = row_dict.get("type", "chunk")
|
|
358
|
+
category = self.TIER1_CATEGORIES.get(mem_type, "Patterns")
|
|
359
|
+
|
|
360
|
+
if category not in categorized:
|
|
361
|
+
categorized[category] = []
|
|
362
|
+
categorized[category].append(row_dict)
|
|
363
|
+
|
|
364
|
+
# Build the markdown string, respecting line and char budgets
|
|
365
|
+
markdown = self._format_tier1_section(categorized)
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
"success": True,
|
|
369
|
+
"markdown": markdown,
|
|
370
|
+
"memory_count": len(rows),
|
|
371
|
+
"categories": {cat: len(items) for cat, items in categorized.items()}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
def _format_tier1_section(self, categorized: Dict[str, List[Dict[str, Any]]]) -> str:
|
|
375
|
+
"""Format categorized memories into a markdown section with markers.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
categorized: Dict mapping category name to list of memory dicts
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Complete markdown string including start/end markers.
|
|
382
|
+
"""
|
|
383
|
+
lines: List[str] = []
|
|
384
|
+
lines.append(self.TIER1_START_MARKER)
|
|
385
|
+
lines.append("## Auto-Generated Memory Context")
|
|
386
|
+
lines.append("")
|
|
387
|
+
lines.append(f"*Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*")
|
|
388
|
+
lines.append("")
|
|
389
|
+
|
|
390
|
+
if not categorized:
|
|
391
|
+
lines.append("*No memories qualify for Tier 1 context yet.*")
|
|
392
|
+
lines.append("")
|
|
393
|
+
lines.append(self.TIER1_END_MARKER)
|
|
394
|
+
return "\n".join(lines)
|
|
395
|
+
|
|
396
|
+
total_chars = sum(len(l) for l in lines)
|
|
397
|
+
|
|
398
|
+
# Ordered category output
|
|
399
|
+
category_order = ["Decisions", "Patterns", "Known Issues", "Preferences"]
|
|
400
|
+
for cat_name in category_order:
|
|
401
|
+
if cat_name not in categorized:
|
|
402
|
+
continue
|
|
403
|
+
|
|
404
|
+
items = categorized[cat_name]
|
|
405
|
+
# Budget check
|
|
406
|
+
if len(lines) >= self.TIER1_MAX_LINES - 5:
|
|
407
|
+
break
|
|
408
|
+
if total_chars >= self.TIER1_MAX_CHARS - 200:
|
|
409
|
+
break
|
|
410
|
+
|
|
411
|
+
lines.append(f"### {cat_name}")
|
|
412
|
+
lines.append("")
|
|
413
|
+
|
|
414
|
+
for item in items:
|
|
415
|
+
if len(lines) >= self.TIER1_MAX_LINES - 2:
|
|
416
|
+
break
|
|
417
|
+
if total_chars >= self.TIER1_MAX_CHARS - 100:
|
|
418
|
+
break
|
|
419
|
+
|
|
420
|
+
entry_line = self._format_tier1_entry(item)
|
|
421
|
+
lines.append(entry_line)
|
|
422
|
+
total_chars += len(entry_line)
|
|
423
|
+
|
|
424
|
+
lines.append("")
|
|
425
|
+
|
|
426
|
+
lines.append(self.TIER1_END_MARKER)
|
|
427
|
+
return "\n".join(lines)
|
|
428
|
+
|
|
429
|
+
def _format_tier1_entry(self, memory: Dict[str, Any]) -> str:
|
|
430
|
+
"""Format a single memory as a concise bullet point.
|
|
431
|
+
|
|
432
|
+
Truncates content to TIER1_CONTENT_TRUNCATE chars and adds
|
|
433
|
+
metadata annotations for importance and project.
|
|
434
|
+
"""
|
|
435
|
+
content = memory.get("content", "")
|
|
436
|
+
# Extract the first meaningful line (skip markdown headers)
|
|
437
|
+
first_line = ""
|
|
438
|
+
for line in content.split("\n"):
|
|
439
|
+
stripped = line.strip()
|
|
440
|
+
if stripped and not stripped.startswith("#"):
|
|
441
|
+
first_line = stripped
|
|
442
|
+
break
|
|
443
|
+
|
|
444
|
+
if not first_line:
|
|
445
|
+
first_line = content.replace("\n", " ").strip()
|
|
446
|
+
|
|
447
|
+
# Truncate
|
|
448
|
+
if len(first_line) > self.TIER1_CONTENT_TRUNCATE:
|
|
449
|
+
first_line = first_line[:self.TIER1_CONTENT_TRUNCATE].rstrip() + "..."
|
|
450
|
+
|
|
451
|
+
# Build annotations
|
|
452
|
+
annotations = []
|
|
453
|
+
importance = memory.get("importance", 5)
|
|
454
|
+
if importance >= 8:
|
|
455
|
+
annotations.append(f"imp:{importance}")
|
|
456
|
+
|
|
457
|
+
project_name = memory.get("project_name") or ""
|
|
458
|
+
if project_name:
|
|
459
|
+
annotations.append(project_name)
|
|
460
|
+
|
|
461
|
+
success = memory.get("success")
|
|
462
|
+
if success == 0:
|
|
463
|
+
annotations.append("failed")
|
|
464
|
+
elif memory.get("type") == "error" and success == 1:
|
|
465
|
+
annotations.append("fixed")
|
|
466
|
+
|
|
467
|
+
suffix = f" [{', '.join(annotations)}]" if annotations else ""
|
|
468
|
+
return f"- {first_line}{suffix}"
|
|
469
|
+
|
|
470
|
+
async def write_tier1_to_claude_md(
|
|
471
|
+
self,
|
|
472
|
+
project_path: Optional[str] = None,
|
|
473
|
+
claude_md_path: Optional[Path] = None,
|
|
474
|
+
dry_run: bool = False
|
|
475
|
+
) -> Dict[str, Any]:
|
|
476
|
+
"""Generate Tier 1 context and write it into CLAUDE.md.
|
|
477
|
+
|
|
478
|
+
Reads the existing CLAUDE.md, finds and replaces the auto-generated
|
|
479
|
+
section (between TIER1_START_MARKER and TIER1_END_MARKER), or appends
|
|
480
|
+
it if no markers exist. All manually-written content is preserved.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
project_path: Optional filter to a specific project
|
|
484
|
+
claude_md_path: Path to CLAUDE.md (default: user's global)
|
|
485
|
+
dry_run: If True, return the result without writing
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
Dict with 'success', 'path', 'memory_count', 'lines_written',
|
|
489
|
+
'categories', and optionally 'preview' for dry_run
|
|
490
|
+
"""
|
|
491
|
+
path = claude_md_path or USER_CLAUDE_MD
|
|
492
|
+
|
|
493
|
+
# Generate tier 1 content
|
|
494
|
+
result = await self.auto_generate_tier1(project_path=project_path)
|
|
495
|
+
if not result.get("success"):
|
|
496
|
+
return result
|
|
497
|
+
|
|
498
|
+
tier1_markdown = result["markdown"]
|
|
499
|
+
|
|
500
|
+
# Read existing file
|
|
501
|
+
current_content = self._read_claude_md(path)
|
|
502
|
+
|
|
503
|
+
# Find existing markers
|
|
504
|
+
start_idx = current_content.find(self.TIER1_START_MARKER)
|
|
505
|
+
end_idx = current_content.find(self.TIER1_END_MARKER)
|
|
506
|
+
|
|
507
|
+
if start_idx != -1 and end_idx != -1:
|
|
508
|
+
# Replace existing section (include the end marker length)
|
|
509
|
+
end_idx += len(self.TIER1_END_MARKER)
|
|
510
|
+
new_content = (
|
|
511
|
+
current_content[:start_idx].rstrip("\n")
|
|
512
|
+
+ "\n\n"
|
|
513
|
+
+ tier1_markdown
|
|
514
|
+
+ "\n"
|
|
515
|
+
+ current_content[end_idx:].lstrip("\n")
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
# Append at the end
|
|
519
|
+
separator = "\n\n" if current_content and not current_content.endswith("\n\n") else ""
|
|
520
|
+
if current_content and current_content.endswith("\n"):
|
|
521
|
+
separator = "\n"
|
|
522
|
+
new_content = current_content + separator + tier1_markdown + "\n"
|
|
523
|
+
|
|
524
|
+
tier1_lines = tier1_markdown.count("\n") + 1
|
|
525
|
+
|
|
526
|
+
if dry_run:
|
|
527
|
+
return {
|
|
528
|
+
"success": True,
|
|
529
|
+
"dry_run": True,
|
|
530
|
+
"path": str(path),
|
|
531
|
+
"memory_count": result["memory_count"],
|
|
532
|
+
"lines_written": tier1_lines,
|
|
533
|
+
"categories": result["categories"],
|
|
534
|
+
"preview": tier1_markdown
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
# Write the file
|
|
538
|
+
try:
|
|
539
|
+
self._write_claude_md(path, new_content)
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.error(f"Failed to write CLAUDE.md: {e}")
|
|
542
|
+
return {
|
|
543
|
+
"success": False,
|
|
544
|
+
"error": f"Write failed: {e}",
|
|
545
|
+
"path": str(path)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
"success": True,
|
|
550
|
+
"path": str(path),
|
|
551
|
+
"memory_count": result["memory_count"],
|
|
552
|
+
"lines_written": tier1_lines,
|
|
553
|
+
"categories": result["categories"]
|
|
554
|
+
}
|
|
555
|
+
|
|
240
556
|
async def suggest_updates(
|
|
241
557
|
self,
|
|
242
558
|
project_path: Optional[str] = None
|