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.
Files changed (91) hide show
  1. package/bin/cli.js +11 -1
  2. package/bin/lib/banner.js +39 -0
  3. package/bin/lib/environment.js +166 -0
  4. package/bin/lib/installer.js +291 -0
  5. package/bin/lib/models.js +95 -0
  6. package/bin/lib/steps/advanced.js +101 -0
  7. package/bin/lib/steps/confirm.js +87 -0
  8. package/bin/lib/steps/model.js +57 -0
  9. package/bin/lib/steps/provider.js +65 -0
  10. package/bin/lib/steps/scope.js +59 -0
  11. package/bin/lib/steps/server.js +74 -0
  12. package/bin/lib/ui.js +75 -0
  13. package/bin/onboarding.js +164 -0
  14. package/bin/postinstall.js +22 -257
  15. package/config.py +103 -4
  16. package/dashboard.html +697 -27
  17. package/hooks/extract_memories.py +439 -0
  18. package/hooks/pre_compact_hook.py +76 -0
  19. package/hooks/session_end_hook.py +149 -0
  20. package/hooks/stop_hook.py +372 -0
  21. package/install.py +91 -37
  22. package/main.py +1636 -892
  23. package/mcp_server.py +451 -0
  24. package/package.json +14 -3
  25. package/requirements.txt +12 -8
  26. package/services/adaptive_ranker.py +272 -0
  27. package/services/agent_catalog.json +153 -0
  28. package/services/agent_registry.py +245 -730
  29. package/services/claude_md_sync.py +320 -4
  30. package/services/consolidation.py +417 -0
  31. package/services/database.py +586 -105
  32. package/services/embedding_pipeline.py +262 -0
  33. package/services/embeddings.py +493 -85
  34. package/services/memory_decay.py +408 -0
  35. package/services/native_memory_paths.py +86 -0
  36. package/services/native_memory_sync.py +496 -0
  37. package/services/response_manager.py +183 -0
  38. package/services/terminal_ui.py +199 -0
  39. package/services/tier_manager.py +235 -0
  40. package/services/websocket.py +26 -6
  41. package/skills/search.py +136 -61
  42. package/skills/session_review.py +210 -23
  43. package/skills/store.py +125 -18
  44. package/terminal_dashboard.py +474 -0
  45. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  46. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  47. package/hooks/__pycache__/grounding-hook.cpython-312.pyc +0 -0
  48. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  49. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  50. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  51. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  52. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  53. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  54. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  55. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  56. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  57. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  58. package/services/__pycache__/curator.cpython-312.pyc +0 -0
  59. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  60. package/services/__pycache__/database.cpython-312.pyc +0 -0
  61. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  62. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  63. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  64. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  65. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  66. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  67. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  68. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  69. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  70. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  71. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  72. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  73. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  74. package/skills/__pycache__/confidence_tracker.cpython-312.pyc +0 -0
  75. package/skills/__pycache__/context.cpython-312.pyc +0 -0
  76. package/skills/__pycache__/curator.cpython-312.pyc +0 -0
  77. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  78. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  81. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  82. package/skills/__pycache__/session_review.cpython-312.pyc +0 -0
  83. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  84. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  85. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  86. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  87. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  88. package/test_automation.py +0 -221
  89. package/test_complete.py +0 -338
  90. package/test_full.py +0 -322
  91. 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