agmem 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

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.
@@ -25,6 +25,18 @@ logging.basicConfig(
25
25
  logger = logging.getLogger("agmem-mcp")
26
26
 
27
27
 
28
+ def _capture_observation(tool_name: str, arguments: dict, result: str) -> None:
29
+ """Capture tool call as observation if daemon is running."""
30
+ try:
31
+ from memvcs.core.daemon import capture_observation
32
+
33
+ capture_observation(tool_name, arguments, result)
34
+ except ImportError:
35
+ pass # Daemon module not available
36
+ except Exception as e:
37
+ logger.debug(f"Observation capture failed: {e}")
38
+
39
+
28
40
  def _get_repo():
29
41
  """Get repository from cwd. Returns (repo, None) or (None, error_msg)."""
30
42
  from memvcs.core.repository import Repository
@@ -129,8 +141,11 @@ def _create_mcp_server():
129
141
  pass
130
142
 
131
143
  if not results:
132
- return f"No matches for '{query}' in memory."
133
- return "\n\n".join(results[:10])
144
+ result = f"No matches for '{query}' in memory."
145
+ else:
146
+ result = "\n\n".join(results[:10])
147
+ _capture_observation("memory_search", {"query": query, "memory_type": memory_type}, result)
148
+ return result
134
149
 
135
150
  @mcp.tool()
136
151
  def memory_add(path: str, commit: bool = False, message: str = "") -> str:
@@ -164,10 +179,14 @@ def _create_mcp_server():
164
179
  return "Staged. Error: message required for commit."
165
180
  try:
166
181
  commit_hash = repo.commit(message)
167
- return f"Staged and committed: {rel_path} ({commit_hash[:8]})"
182
+ result = f"Staged and committed: {rel_path} ({commit_hash[:8]})"
183
+ _capture_observation("memory_add", {"path": path, "commit": True}, result)
184
+ return result
168
185
  except Exception as e:
169
186
  return f"Staged. Error committing: {e}"
170
- return f"Staged: {rel_path}. Run 'agmem commit -m \"message\"' to save."
187
+ result = f"Staged: {rel_path}. Run 'agmem commit -m \"message\"' to save."
188
+ _capture_observation("memory_add", {"path": path, "commit": False}, result)
189
+ return result
171
190
 
172
191
  @mcp.tool()
173
192
  def memory_log(max_count: int = 10) -> str:
@@ -251,6 +270,758 @@ def _create_mcp_server():
251
270
 
252
271
  return full_path.read_text(encoding="utf-8", errors="replace")
253
272
 
273
+ # --- Progressive Disclosure Search Tools ---
274
+
275
+ @mcp.tool()
276
+ def memory_index(query: str, memory_type: Optional[str] = None, limit: int = 20) -> str:
277
+ """Layer 1: Lightweight search returning metadata + first line only.
278
+
279
+ Use this first to find relevant memories with minimal token cost.
280
+ Follow up with memory_details for full content of specific files.
281
+
282
+ Args:
283
+ query: Search query
284
+ memory_type: Filter by type (episodic, semantic, procedural)
285
+ limit: Maximum results (default 20)
286
+ """
287
+ repo, err = _get_repo()
288
+ if err:
289
+ return f"Error: {err}"
290
+
291
+ try:
292
+ from memvcs.core.search_index import SearchIndex, layer1_cost
293
+
294
+ index = SearchIndex(repo.mem_dir)
295
+ # Ensure index is up to date
296
+ index.index_directory(repo.current_dir)
297
+
298
+ results = index.search_index(query, memory_type=memory_type, limit=limit)
299
+ index.close()
300
+
301
+ if not results:
302
+ return f"No results for: {query}"
303
+
304
+ lines = [f"Found {len(results)} results (est. ~{layer1_cost(results)} tokens):"]
305
+ for r in results:
306
+ lines.append(f"- [{r.memory_type}] {r.filename}: {r.first_line[:80]}...")
307
+ lines.append(f" Path: {r.path}")
308
+ return "\n".join(lines)
309
+ except Exception as e:
310
+ return f"Error: {e}"
311
+
312
+ @mcp.tool()
313
+ def memory_timeline(days: int = 7, limit: int = 10) -> str:
314
+ """Layer 2: Get timeline of memory activity grouped by date.
315
+
316
+ Shows what was captured each day without full content.
317
+
318
+ Args:
319
+ days: Number of days to look back (default 7)
320
+ limit: Maximum entries per day (default 10)
321
+ """
322
+ repo, err = _get_repo()
323
+ if err:
324
+ return f"Error: {err}"
325
+
326
+ try:
327
+ from memvcs.core.search_index import SearchIndex
328
+ from datetime import datetime, timedelta, timezone
329
+
330
+ index = SearchIndex(repo.mem_dir)
331
+ index.index_directory(repo.current_dir)
332
+
333
+ start_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
334
+ timeline = index.get_timeline(start_date=start_date, limit=limit)
335
+ index.close()
336
+
337
+ if not timeline:
338
+ return "No activity in timeline."
339
+
340
+ lines = [f"Memory timeline (last {days} days):"]
341
+ for entry in timeline:
342
+ lines.append(f"\n## {entry.date} ({entry.file_count} files)")
343
+ for f in entry.files[:5]:
344
+ lines.append(f" - [{f['memory_type']}] {f['filename']}")
345
+ if len(entry.files) > 5:
346
+ lines.append(f" ... and {len(entry.files) - 5} more")
347
+ return "\n".join(lines)
348
+ except Exception as e:
349
+ return f"Error: {e}"
350
+
351
+ @mcp.tool()
352
+ def memory_details(paths: str) -> str:
353
+ """Layer 3: Get full content of specific memory files.
354
+
355
+ Use after memory_index to retrieve complete content.
356
+ Higher token cost - use sparingly.
357
+
358
+ Args:
359
+ paths: Comma-separated list of file paths from memory_index results
360
+ """
361
+ repo, err = _get_repo()
362
+ if err:
363
+ return f"Error: {err}"
364
+
365
+ try:
366
+ from memvcs.core.search_index import SearchIndex, layer3_cost
367
+
368
+ path_list = [p.strip() for p in paths.split(",")]
369
+
370
+ index = SearchIndex(repo.mem_dir)
371
+ details = index.get_full_details(path_list)
372
+ index.close()
373
+
374
+ if not details:
375
+ return "No files found."
376
+
377
+ lines = [f"Retrieved {len(details)} files (est. ~{layer3_cost(details)} tokens):"]
378
+ for d in details:
379
+ lines.append(f"\n---\n## {d['filename']}\n")
380
+ lines.append(d["content"])
381
+ return "\n".join(lines)
382
+ except Exception as e:
383
+ return f"Error: {e}"
384
+
385
+ # --- Session Management Tools ---
386
+
387
+ @mcp.tool()
388
+ def session_start(context: Optional[str] = None) -> str:
389
+ """Start a new work session for automatic memory capture.
390
+
391
+ Sessions group related observations and create semantic commits.
392
+
393
+ Args:
394
+ context: Optional project or task context description
395
+ """
396
+ repo, err = _get_repo()
397
+ if err:
398
+ return f"Error: {err}"
399
+
400
+ try:
401
+ from memvcs.core.session import SessionManager
402
+
403
+ manager = SessionManager(repo.root)
404
+ session = manager.start_session(project_context=context)
405
+
406
+ return f"Session started: {session.id}\nContext: {context or 'None'}\nStatus: {session.status}"
407
+ except Exception as e:
408
+ return f"Error: {e}"
409
+
410
+ @mcp.tool()
411
+ def session_status() -> str:
412
+ """Get current session status and statistics."""
413
+ repo, err = _get_repo()
414
+ if err:
415
+ return f"Error: {err}"
416
+
417
+ try:
418
+ from memvcs.core.session import SessionManager
419
+
420
+ manager = SessionManager(repo.root)
421
+ status = manager.get_status()
422
+
423
+ if not status.get("active"):
424
+ return "No active session. Use session_start to begin."
425
+
426
+ lines = [
427
+ f"Session: {status['session_id']}",
428
+ f"Status: {status['status']}",
429
+ f"Started: {status['started_at']}",
430
+ f"Observations: {status['observation_count']}",
431
+ f"Topics: {', '.join(status.get('topics', [])) or 'None'}",
432
+ f"Commits: {status['commit_count']}",
433
+ ]
434
+ return "\n".join(lines)
435
+ except Exception as e:
436
+ return f"Error: {e}"
437
+
438
+ @mcp.tool()
439
+ def session_commit(end_session: bool = False) -> str:
440
+ """Commit current session observations to memory.
441
+
442
+ Args:
443
+ end_session: If True, also end the session after committing
444
+ """
445
+ repo, err = _get_repo()
446
+ if err:
447
+ return f"Error: {err}"
448
+
449
+ try:
450
+ from memvcs.core.session import SessionManager
451
+
452
+ manager = SessionManager(repo.root)
453
+
454
+ if not manager.session:
455
+ return "No active session."
456
+
457
+ if end_session:
458
+ commit_hash = manager.end_session(commit=True)
459
+ return f"Session ended and committed: {commit_hash or 'no changes'}"
460
+ else:
461
+ commit_hash = manager._commit_session()
462
+ return f"Session committed: {commit_hash or 'no changes'}"
463
+ except Exception as e:
464
+ return f"Error: {e}"
465
+
466
+ @mcp.tool()
467
+ def session_end(commit: bool = True) -> str:
468
+ """End the current work session.
469
+
470
+ Args:
471
+ commit: If True, commit observations before ending
472
+ """
473
+ repo, err = _get_repo()
474
+ if err:
475
+ return f"Error: {err}"
476
+
477
+ try:
478
+ from memvcs.core.session import SessionManager
479
+
480
+ manager = SessionManager(repo.root)
481
+ commit_hash = manager.end_session(commit=commit)
482
+ return f"Session ended. Commit: {commit_hash or 'no changes'}"
483
+ except Exception as e:
484
+ return f"Error: {e}"
485
+
486
+ # --- Collaboration Tools ---
487
+
488
+ @mcp.tool()
489
+ def agent_register(name: str, agent_type: str = "assistant") -> str:
490
+ """Register a new agent in the collaboration registry.
491
+
492
+ Args:
493
+ name: Agent name (e.g., "Claude", "GPT-4")
494
+ agent_type: Type of agent ("assistant", "human", "system")
495
+ """
496
+ repo, err = _get_repo()
497
+ if err:
498
+ return f"Error: {err}"
499
+
500
+ try:
501
+ from memvcs.core.collaboration import AgentRegistry
502
+
503
+ registry = AgentRegistry(repo.mem_dir)
504
+ agent = registry.register_agent(name, metadata={"type": agent_type})
505
+ return f"Agent registered: {agent.agent_id} ({agent.name})"
506
+ except Exception as e:
507
+ return f"Error: {e}"
508
+
509
+ @mcp.tool()
510
+ def trust_grant(from_agent: str, to_agent: str, level: str = "partial") -> str:
511
+ """Grant trust from one agent to another.
512
+
513
+ Args:
514
+ from_agent: Agent ID granting trust
515
+ to_agent: Agent ID receiving trust
516
+ level: Trust level ("full", "partial", "read-only", "none")
517
+ """
518
+ repo, err = _get_repo()
519
+ if err:
520
+ return f"Error: {err}"
521
+
522
+ try:
523
+ from memvcs.core.collaboration import TrustManager
524
+
525
+ trust_mgr = TrustManager(repo.mem_dir)
526
+ relation = trust_mgr.grant_trust(from_agent, to_agent, level)
527
+ return f"Trust granted: {from_agent} -> {to_agent} ({level})"
528
+ except Exception as e:
529
+ return f"Error: {e}"
530
+
531
+ @mcp.tool()
532
+ def contributions_list(limit: int = 10) -> str:
533
+ """Get contributor leaderboard.
534
+
535
+ Args:
536
+ limit: Maximum contributors to show
537
+ """
538
+ repo, err = _get_repo()
539
+ if err:
540
+ return f"Error: {err}"
541
+
542
+ try:
543
+ from memvcs.core.collaboration import ContributionTracker
544
+
545
+ tracker = ContributionTracker(repo.mem_dir)
546
+ leaderboard = tracker.get_leaderboard(limit=limit)
547
+
548
+ if not leaderboard:
549
+ return "No contributions recorded yet."
550
+
551
+ lines = ["Contribution Leaderboard:"]
552
+ for entry in leaderboard:
553
+ lines.append(
554
+ f"#{entry['rank']} {entry['agent_id'][:8]}: {entry['commits']} commits"
555
+ )
556
+ return "\n".join(lines)
557
+ except Exception as e:
558
+ return f"Error: {e}"
559
+
560
+ # --- Compliance Tools ---
561
+
562
+ @mcp.tool()
563
+ def privacy_status(budget_name: Optional[str] = None) -> str:
564
+ """Get privacy budget status.
565
+
566
+ Args:
567
+ budget_name: Specific budget to check, or None for all
568
+ """
569
+ repo, err = _get_repo()
570
+ if err:
571
+ return f"Error: {err}"
572
+
573
+ try:
574
+ from memvcs.core.compliance import PrivacyManager
575
+
576
+ mgr = PrivacyManager(repo.mem_dir)
577
+
578
+ if budget_name:
579
+ budget = mgr.get_budget(budget_name)
580
+ if not budget:
581
+ return f"Budget not found: {budget_name}"
582
+ return f"Budget '{budget_name}': {budget.remaining():.2%} remaining ({budget.queries_made} queries)"
583
+ else:
584
+ data = mgr.get_dashboard_data()
585
+ if not data["budgets"]:
586
+ return "No privacy budgets configured."
587
+ lines = ["Privacy Budgets:"]
588
+ for b in data["budgets"]:
589
+ lines.append(f"- {b['name']}: {b['remaining']:.2%} remaining")
590
+ return "\n".join(lines)
591
+ except Exception as e:
592
+ return f"Error: {e}"
593
+
594
+ @mcp.tool()
595
+ def integrity_verify() -> str:
596
+ """Verify memory integrity using Merkle tree."""
597
+ repo, err = _get_repo()
598
+ if err:
599
+ return f"Error: {err}"
600
+
601
+ try:
602
+ from memvcs.core.compliance import TamperDetector
603
+
604
+ detector = TamperDetector(repo.mem_dir)
605
+ result = detector.verify_integrity(repo.current_dir)
606
+
607
+ if result.get("error"):
608
+ # First time - store baseline
609
+ detector.store_merkle_state(repo.current_dir)
610
+ return "No baseline found. Created new integrity baseline."
611
+
612
+ if result["verified"]:
613
+ return "✓ Integrity verified. No tampering detected."
614
+ else:
615
+ lines = ["⚠ Integrity check failed:"]
616
+ if result["modified_files"]:
617
+ lines.append(f" Modified: {', '.join(result['modified_files'][:5])}")
618
+ if result["added_files"]:
619
+ lines.append(f" Added: {', '.join(result['added_files'][:5])}")
620
+ if result["deleted_files"]:
621
+ lines.append(f" Deleted: {', '.join(result['deleted_files'][:5])}")
622
+ return "\n".join(lines)
623
+ except Exception as e:
624
+ return f"Error: {e}"
625
+
626
+ # --- Archaeology Tools ---
627
+
628
+ @mcp.tool()
629
+ def forgotten_memories(days: int = 30, limit: int = 10) -> str:
630
+ """Find memories that haven't been accessed recently.
631
+
632
+ Args:
633
+ days: Threshold for "forgotten" (default 30 days)
634
+ limit: Maximum results
635
+ """
636
+ repo, err = _get_repo()
637
+ if err:
638
+ return f"Error: {err}"
639
+
640
+ try:
641
+ from memvcs.core.archaeology import ForgottenKnowledgeFinder
642
+
643
+ finder = ForgottenKnowledgeFinder(repo.root)
644
+ forgotten = finder.find_forgotten(days_threshold=days, limit=limit)
645
+
646
+ if not forgotten:
647
+ return f"No memories older than {days} days found."
648
+
649
+ lines = [f"Forgotten memories (>{days} days):"]
650
+ for m in forgotten:
651
+ lines.append(f"- {m.path} ({m.days_since_access}d ago)")
652
+ lines.append(f" Preview: {m.content_preview[:60]}...")
653
+ return "\n".join(lines)
654
+ except Exception as e:
655
+ return f"Error: {e}"
656
+
657
+ @mcp.tool()
658
+ def find_context(path: str, date: str, window_days: int = 7) -> str:
659
+ """Find what was happening around a memory at a point in time.
660
+
661
+ Args:
662
+ path: Memory file path
663
+ date: Target date (YYYY-MM-DD format)
664
+ window_days: Days before/after to search
665
+ """
666
+ repo, err = _get_repo()
667
+ if err:
668
+ return f"Error: {err}"
669
+
670
+ try:
671
+ from memvcs.core.archaeology import ContextReconstructor
672
+
673
+ reconstructor = ContextReconstructor(repo.root)
674
+ context = reconstructor.reconstruct_context(path, date, window_days)
675
+
676
+ if context.get("error"):
677
+ return f"Error: {context['error']}"
678
+
679
+ return f"Context for {path} around {date}:\n{context['summary']}"
680
+ except Exception as e:
681
+ return f"Error: {e}"
682
+
683
+ # --- Confidence Tools ---
684
+
685
+ @mcp.tool()
686
+ def confidence_score(path: str, source_id: Optional[str] = None) -> str:
687
+ """Get or calculate confidence score for a memory.
688
+
689
+ Args:
690
+ path: Memory file path
691
+ source_id: Optional source agent ID
692
+ """
693
+ repo, err = _get_repo()
694
+ if err:
695
+ return f"Error: {err}"
696
+
697
+ try:
698
+ from memvcs.core.confidence import ConfidenceCalculator
699
+
700
+ calculator = ConfidenceCalculator(repo.mem_dir)
701
+
702
+ # Check file exists
703
+ full_path = repo.current_dir / path
704
+ created_at = None
705
+ if full_path.exists():
706
+ from datetime import datetime, timezone
707
+
708
+ mtime = full_path.stat().st_mtime
709
+ created_at = datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat()
710
+
711
+ score = calculator.calculate_score(path, source_id=source_id, created_at=created_at)
712
+
713
+ lines = [
714
+ f"Confidence for {path}: {score.score:.1%}",
715
+ f" Source reliability: {score.factors.source_reliability:.1%}",
716
+ f" Age: {score.factors.age_days:.1f} days",
717
+ f" Corroborations: {score.factors.corroboration_count}",
718
+ f" Contradictions: {score.factors.contradiction_count}",
719
+ ]
720
+ return "\n".join(lines)
721
+ except Exception as e:
722
+ return f"Error: {e}"
723
+
724
+ @mcp.tool()
725
+ def low_confidence(threshold: float = 0.5, limit: int = 10) -> str:
726
+ """Find memories with low confidence scores.
727
+
728
+ Args:
729
+ threshold: Confidence threshold (0.0-1.0)
730
+ limit: Maximum results
731
+ """
732
+ repo, err = _get_repo()
733
+ if err:
734
+ return f"Error: {err}"
735
+
736
+ try:
737
+ from memvcs.core.confidence import ConfidenceCalculator
738
+
739
+ calculator = ConfidenceCalculator(repo.mem_dir)
740
+ low = calculator.get_low_confidence_memories(threshold=threshold)
741
+
742
+ if not low:
743
+ return f"No memories below {threshold:.0%} confidence."
744
+
745
+ lines = [f"Low confidence memories (<{threshold:.0%}):"]
746
+ for m in low[:limit]:
747
+ lines.append(f"- {m['path']}: {m['score']:.1%}")
748
+ return "\n".join(lines)
749
+ except Exception as e:
750
+ return f"Error: {e}"
751
+
752
+ @mcp.tool()
753
+ def expiring_soon(days: int = 7, threshold: float = 0.5) -> str:
754
+ """Find memories that will drop below confidence threshold soon.
755
+
756
+ Args:
757
+ days: Days to look ahead
758
+ threshold: Confidence threshold
759
+ """
760
+ repo, err = _get_repo()
761
+ if err:
762
+ return f"Error: {err}"
763
+
764
+ try:
765
+ from memvcs.core.confidence import ConfidenceCalculator
766
+
767
+ calculator = ConfidenceCalculator(repo.mem_dir)
768
+ expiring = calculator.get_expiring_soon(days=days, threshold=threshold)
769
+
770
+ if not expiring:
771
+ return f"No memories expiring in the next {days} days."
772
+
773
+ lines = [f"Expiring within {days} days:"]
774
+ for m in expiring:
775
+ lines.append(
776
+ f"- {m['path']}: {m['current_score']:.1%} -> <{threshold:.0%} in {m['days_until_threshold']:.1f}d"
777
+ )
778
+ return "\n".join(lines)
779
+ except Exception as e:
780
+ return f"Error: {e}"
781
+
782
+ # --- Time Travel Tools ---
783
+
784
+ @mcp.tool()
785
+ def time_travel(time_expr: str) -> str:
786
+ """Find memory state at a specific time.
787
+
788
+ Args:
789
+ time_expr: Time expression like "2 days ago", "yesterday", "2024-01-15"
790
+ """
791
+ repo, err = _get_repo()
792
+ if err:
793
+ return f"Error: {err}"
794
+
795
+ try:
796
+ from memvcs.core.timetravel import TemporalNavigator
797
+
798
+ navigator = TemporalNavigator(repo.root)
799
+ commit = navigator.find_commit_at(time_expr)
800
+
801
+ if not commit:
802
+ return f"No commits found at or before: {time_expr}"
803
+
804
+ return f"At {time_expr}:\nCommit: {commit['short_hash']}\nMessage: {commit['message']}\nTime: {commit.get('timestamp', 'unknown')}"
805
+ except Exception as e:
806
+ return f"Error: {e}"
807
+
808
+ @mcp.tool()
809
+ def timeline(days: int = 30) -> str:
810
+ """Get activity timeline.
811
+
812
+ Args:
813
+ days: Number of days to include
814
+ """
815
+ repo, err = _get_repo()
816
+ if err:
817
+ return f"Error: {err}"
818
+
819
+ try:
820
+ from memvcs.core.timetravel import TimelineVisualizer
821
+
822
+ visualizer = TimelineVisualizer(repo.root)
823
+ timeline_data = visualizer.get_activity_timeline(days=days)
824
+
825
+ if not timeline_data:
826
+ return f"No activity in the last {days} days."
827
+
828
+ lines = [f"Activity (last {days} days):"]
829
+ for day in timeline_data[-10:]:
830
+ lines.append(f" {day['period']}: {day['count']} commits")
831
+
832
+ total = sum(d["count"] for d in timeline_data)
833
+ lines.append(f"\nTotal: {total} commits")
834
+ return "\n".join(lines)
835
+ except Exception as e:
836
+ return f"Error: {e}"
837
+
838
+ # --- Semantic Graph Tools ---
839
+
840
+ @mcp.tool()
841
+ def memory_graph(limit: int = 20) -> str:
842
+ """Get semantic memory graph summary.
843
+
844
+ Args:
845
+ limit: Maximum nodes to include
846
+ """
847
+ repo, err = _get_repo()
848
+ if err:
849
+ return f"Error: {err}"
850
+
851
+ try:
852
+ from memvcs.core.semantic_graph import get_semantic_graph_dashboard
853
+
854
+ dashboard = get_semantic_graph_dashboard(repo.root)
855
+
856
+ lines = [
857
+ f"Memory Graph: {dashboard['node_count']} nodes, {dashboard['edge_count']} edges",
858
+ "",
859
+ "By Type:",
860
+ ]
861
+ for t, count in dashboard.get("clusters_by_type", {}).items():
862
+ lines.append(f" {t}: {count}")
863
+
864
+ top_tags = list(dashboard.get("clusters_by_tag", {}).items())[:5]
865
+ if top_tags:
866
+ lines.append("\nTop Tags:")
867
+ for tag, count in top_tags:
868
+ lines.append(f" #{tag}: {count} memories")
869
+
870
+ return "\n".join(lines)
871
+ except Exception as e:
872
+ return f"Error: {e}"
873
+
874
+ @mcp.tool()
875
+ def graph_related(path: str, depth: int = 2) -> str:
876
+ """Find related memories using graph traversal.
877
+
878
+ Args:
879
+ path: Memory file path
880
+ depth: Maximum traversal depth
881
+ """
882
+ repo, err = _get_repo()
883
+ if err:
884
+ return f"Error: {err}"
885
+
886
+ try:
887
+ from memvcs.core.semantic_graph import SemanticGraphBuilder, GraphSearchEngine
888
+
889
+ builder = SemanticGraphBuilder(repo.root)
890
+ nodes, edges = builder.build_graph()
891
+
892
+ # Find node by path
893
+ node_id = None
894
+ for n in nodes:
895
+ if n.path == path or path in n.path:
896
+ node_id = n.node_id
897
+ break
898
+
899
+ if not node_id:
900
+ return f"Memory not found: {path}"
901
+
902
+ nodes_dict = {n.node_id: n for n in nodes}
903
+ engine = GraphSearchEngine(nodes_dict, edges)
904
+ related = engine.find_related(node_id, max_depth=depth, limit=10)
905
+
906
+ if not related:
907
+ return "No related memories found."
908
+
909
+ lines = [f"Related to {path}:"]
910
+ for node, score, dist in related:
911
+ lines.append(f" {node.path} (score: {score:.2f}, depth: {dist})")
912
+ return "\n".join(lines)
913
+ except Exception as e:
914
+ return f"Error: {e}"
915
+
916
+ # --- Agent Tools ---
917
+
918
+ @mcp.tool()
919
+ def agent_health() -> str:
920
+ """Run memory health check."""
921
+ repo, err = _get_repo()
922
+ if err:
923
+ return f"Error: {err}"
924
+
925
+ try:
926
+ from memvcs.core.agents import MemoryAgentManager
927
+
928
+ manager = MemoryAgentManager(repo.root)
929
+ health = manager.run_health_check()
930
+
931
+ lines = ["Memory Health Check:"]
932
+
933
+ cons = health["checks"]["consolidation"]
934
+ lines.append(f" Consolidation candidates: {cons['candidate_count']}")
935
+
936
+ clean = health["checks"]["cleanup"]
937
+ lines.append(f" Cleanup candidates: {clean['candidate_count']}")
938
+
939
+ dups = health["checks"]["duplicates"]
940
+ lines.append(f" Duplicate groups: {dups['duplicate_groups']}")
941
+
942
+ alerts = health.get("alerts", [])
943
+ lines.append(f" Active alerts: {len(alerts)}")
944
+
945
+ return "\n".join(lines)
946
+ except Exception as e:
947
+ return f"Error: {e}"
948
+
949
+ @mcp.tool()
950
+ def find_duplicates() -> str:
951
+ """Find duplicate memory files."""
952
+ repo, err = _get_repo()
953
+ if err:
954
+ return f"Error: {err}"
955
+
956
+ try:
957
+ from memvcs.core.agents import CleanupAgent
958
+
959
+ agent = CleanupAgent(repo.root)
960
+ duplicates = agent.find_duplicates()
961
+
962
+ if not duplicates:
963
+ return "No duplicate memories found."
964
+
965
+ lines = [f"Found {len(duplicates)} duplicate groups:"]
966
+ for dup in duplicates[:10]:
967
+ lines.append(f"\nGroup ({dup['count']} files):")
968
+ for f in dup["files"][:3]:
969
+ lines.append(f" - {f}")
970
+ return "\n".join(lines)
971
+ except Exception as e:
972
+ return f"Error: {e}"
973
+
974
+ @mcp.tool()
975
+ def consolidation_candidates() -> str:
976
+ """Find memories that could be consolidated."""
977
+ repo, err = _get_repo()
978
+ if err:
979
+ return f"Error: {err}"
980
+
981
+ try:
982
+ from memvcs.core.agents import ConsolidationAgent
983
+
984
+ agent = ConsolidationAgent(repo.root)
985
+ candidates = agent.find_consolidation_candidates()
986
+
987
+ if not candidates:
988
+ return "No consolidation candidates found."
989
+
990
+ lines = ["Consolidation candidates:"]
991
+ for c in candidates[:10]:
992
+ lines.append(f"\n{c['prefix']}: {c['file_count']} files")
993
+ lines.append(f" {c['suggestion']}")
994
+ return "\n".join(lines)
995
+ except Exception as e:
996
+ return f"Error: {e}"
997
+
998
+ @mcp.tool()
999
+ def cleanup_candidates(max_age_days: int = 90) -> str:
1000
+ """Find old memories that could be cleaned up.
1001
+
1002
+ Args:
1003
+ max_age_days: Minimum age in days
1004
+ """
1005
+ repo, err = _get_repo()
1006
+ if err:
1007
+ return f"Error: {err}"
1008
+
1009
+ try:
1010
+ from memvcs.core.agents import CleanupAgent
1011
+
1012
+ agent = CleanupAgent(repo.root)
1013
+ candidates = agent.find_cleanup_candidates(max_age_days=max_age_days)
1014
+
1015
+ if not candidates:
1016
+ return f"No memories older than {max_age_days} days."
1017
+
1018
+ lines = [f"Old memories (>{max_age_days} days):"]
1019
+ for c in candidates[:10]:
1020
+ lines.append(f" {c['path']} ({c['age_days']}d old)")
1021
+ return "\n".join(lines)
1022
+ except Exception as e:
1023
+ return f"Error: {e}"
1024
+
254
1025
  return mcp
255
1026
 
256
1027