claude-memory-agent 3.0.2 → 3.1.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/install.py CHANGED
@@ -542,11 +542,18 @@ def setup_hooks(config: Dict[str, str]) -> bool:
542
542
  print_warning("Hooks directory not found in agent, skipping hook setup")
543
543
  return True
544
544
 
545
- # Hooks to install
545
+ # Hooks to install (all 8 active hooks)
546
546
  hooks_to_install = [
547
547
  "session_start.py",
548
- "session_end.py",
548
+ "session_end_hook.py",
549
+ "stop_hook.py",
549
550
  "grounding-hook.py",
551
+ "log-user-request.py",
552
+ "detect-correction.py",
553
+ "pre-tool-decision.py",
554
+ "log-tool-use.py",
555
+ "auto-detect-response.py",
556
+ "extract_memories.py", # Required by session_end_hook
550
557
  ]
551
558
 
552
559
  installed = 0
@@ -574,54 +581,126 @@ def setup_hooks(config: Dict[str, str]) -> bool:
574
581
 
575
582
 
576
583
  def configure_hooks_json(auto: bool = False) -> bool:
577
- """Configure hooks.json to enable the hooks."""
578
- hooks_file = get_claude_settings_dir() / "hooks.json"
584
+ """Configure hooks in settings.json (Claude Code hook format).
579
585
 
580
- # Default hooks configuration
586
+ Writes hooks config directly into ~/.claude/settings.json
587
+ using the correct Claude Code settings format.
588
+ """
589
+ settings_file = get_claude_settings_file()
590
+ python_exe = sys.executable
591
+ hooks_dir = get_hooks_dir()
592
+
593
+ # Build the hooks config in Claude Code settings.json format
581
594
  hooks_config = {
582
- "hooks": {
583
- "UserPromptSubmit": [
584
- {
585
- "command": f"{sys.executable} {get_hooks_dir() / 'session_start.py'}",
586
- "description": "Initialize memory session",
587
- "timeout": 5000
588
- },
589
- {
590
- "command": f"{sys.executable} {get_hooks_dir() / 'grounding-hook.py'}",
591
- "description": "Inject grounding context",
592
- "timeout": 3000
593
- }
594
- ],
595
- "SessionEnd": [
596
- {
597
- "command": f"{sys.executable} {get_hooks_dir() / 'session_end.py'}",
598
- "description": "Save session summary",
599
- "timeout": 10000
600
- }
601
- ]
602
- }
595
+ "UserPromptSubmit": [
596
+ {
597
+ "matcher": "",
598
+ "hooks": [
599
+ {
600
+ "type": "command",
601
+ "command": f"{python_exe} {hooks_dir / 'detect-correction.py'}",
602
+ "async": True,
603
+ },
604
+ {
605
+ "type": "command",
606
+ "command": f"{python_exe} {hooks_dir / 'log-user-request.py'}",
607
+ "async": True,
608
+ },
609
+ {
610
+ "type": "command",
611
+ "command": f"{python_exe} {hooks_dir / 'grounding-hook.py'}",
612
+ },
613
+ ],
614
+ }
615
+ ],
616
+ "PreToolUse": [
617
+ {
618
+ "matcher": "Edit|Write|Bash|Task",
619
+ "hooks": [
620
+ {
621
+ "type": "command",
622
+ "command": f"{python_exe} {hooks_dir / 'pre-tool-decision.py'}",
623
+ "async": True,
624
+ }
625
+ ],
626
+ }
627
+ ],
628
+ "PostToolUse": [
629
+ {
630
+ "matcher": "Edit|Write|Bash|Read",
631
+ "hooks": [
632
+ {
633
+ "type": "command",
634
+ "command": f"{python_exe} {hooks_dir / 'log-tool-use.py'}",
635
+ "async": True,
636
+ }
637
+ ],
638
+ }
639
+ ],
640
+ "Stop": [
641
+ {
642
+ "matcher": "",
643
+ "hooks": [
644
+ {
645
+ "type": "command",
646
+ "command": f"{python_exe} {hooks_dir / 'stop_hook.py'}",
647
+ "async": True,
648
+ },
649
+ {
650
+ "type": "command",
651
+ "command": f"{python_exe} {hooks_dir / 'auto-detect-response.py'}",
652
+ "async": True,
653
+ },
654
+ ],
655
+ }
656
+ ],
657
+ "SessionStart": [
658
+ {
659
+ "matcher": "",
660
+ "hooks": [
661
+ {
662
+ "type": "command",
663
+ "command": f"{python_exe} {hooks_dir / 'session_start.py'}",
664
+ }
665
+ ],
666
+ }
667
+ ],
668
+ "SessionEnd": [
669
+ {
670
+ "matcher": "",
671
+ "hooks": [
672
+ {
673
+ "type": "command",
674
+ "command": f"{python_exe} {hooks_dir / 'session_end_hook.py'}",
675
+ "async": True,
676
+ }
677
+ ],
678
+ }
679
+ ],
603
680
  }
604
681
 
605
- # Merge with existing if present
606
- if hooks_file.exists():
682
+ # Load existing settings
683
+ settings = {}
684
+ if settings_file.exists():
607
685
  try:
608
- existing = json.loads(hooks_file.read_text())
609
- # In auto mode, always merge; otherwise ask
610
- should_update = auto or prompt_yes_no("hooks.json exists. Update with memory agent hooks?", default=True)
611
- if should_update:
612
- if "hooks" not in existing:
613
- existing["hooks"] = {}
614
- existing["hooks"].update(hooks_config["hooks"])
615
- hooks_config = existing
616
- else:
617
- print_success("Keeping existing hooks.json")
618
- return True
686
+ settings = json.loads(settings_file.read_text())
619
687
  except json.JSONDecodeError:
620
- pass
688
+ print_warning(f"Existing {settings_file.name} is invalid, creating backup")
689
+ shutil.copy(settings_file, settings_file.with_suffix(".json.bak"))
690
+ settings = {}
691
+
692
+ # Check if hooks already configured
693
+ if "hooks" in settings:
694
+ should_update = auto or prompt_yes_no("Hooks already configured in settings.json. Update with memory agent hooks?", default=True)
695
+ if not should_update:
696
+ print_success("Keeping existing hooks config")
697
+ return True
698
+
699
+ settings["hooks"] = hooks_config
621
700
 
622
701
  try:
623
- hooks_file.write_text(json.dumps(hooks_config, indent=2))
624
- print_success(f"Configured hooks: {hooks_file}")
702
+ settings_file.write_text(json.dumps(settings, indent=2))
703
+ print_success(f"Configured hooks in: {settings_file}")
625
704
  return True
626
705
  except Exception as e:
627
706
  print_error(f"Failed to configure hooks: {e}")
@@ -762,9 +841,25 @@ def uninstall() -> bool:
762
841
  except Exception as e:
763
842
  print_warning(f"Could not update settings: {e}")
764
843
 
765
- # Remove hooks
844
+ # Remove hooks from settings.json
845
+ if settings_file.exists():
846
+ try:
847
+ settings = json.loads(settings_file.read_text())
848
+ if "hooks" in settings:
849
+ del settings["hooks"]
850
+ settings_file.write_text(json.dumps(settings, indent=2))
851
+ print_success("Removed hooks config from settings.json")
852
+ except Exception as e:
853
+ print_warning(f"Could not update hooks in settings: {e}")
854
+
855
+ # Remove hook files
766
856
  hooks_dir = get_hooks_dir()
767
- hooks_to_remove = ["session_start.py", "session_end.py", "grounding-hook.py"]
857
+ hooks_to_remove = [
858
+ "session_start.py", "session_end_hook.py", "stop_hook.py",
859
+ "grounding-hook.py", "log-user-request.py", "detect-correction.py",
860
+ "pre-tool-decision.py", "log-tool-use.py", "auto-detect-response.py",
861
+ "extract_memories.py",
862
+ ]
768
863
  for hook in hooks_to_remove:
769
864
  hook_file = hooks_dir / hook
770
865
  if hook_file.exists():
package/main.py CHANGED
@@ -111,11 +111,15 @@ from services.claude_md_sync import get_claude_md_sync
111
111
  # Cross-session awareness
112
112
  from services.session_awareness import get_session_awareness
113
113
 
114
+ # Soul layer — persistent personality and learning
115
+ from services.soul import SoulService
116
+
114
117
  # Agent registry for dashboard
115
118
  from services.agent_registry import (
116
119
  AVAILABLE_AGENTS, AVAILABLE_MCPS, AVAILABLE_HOOKS,
117
120
  AGENT_CATEGORIES, get_agents_by_category, get_agent_by_id,
118
- load_configured_hooks, load_configured_mcps
121
+ load_configured_hooks, load_configured_mcps,
122
+ discover_agents, discover_categories, find_agent_by_id, toggle_agent
119
123
  )
120
124
 
121
125
  load_dotenv()
@@ -186,6 +190,7 @@ metrics = OperationMetrics()
186
190
 
187
191
  # Initialize services
188
192
  db = DatabaseService()
193
+ soul_service = SoulService(db)
189
194
  from config import config as _cfg
190
195
  embeddings = EmbeddingService(
191
196
  provider_type=_cfg.EMBEDDING_PROVIDER,
@@ -1772,6 +1777,37 @@ async def _handle_session_append_file(query, params, session_id):
1772
1777
  )
1773
1778
 
1774
1779
 
1780
+ # ============================================================
1781
+ # SOUL LAYER SKILL HANDLERS
1782
+ # ============================================================
1783
+
1784
+ async def _handle_soul_brief(query, params, session_id):
1785
+ project_path = params.get("project_path", "")
1786
+ brief = await soul_service.generate_soul_brief(project_path)
1787
+ return {"success": True, "brief": brief}
1788
+
1789
+ async def _handle_soul_capture(query, params, session_id):
1790
+ sid = params.get("session_id") or session_id or ""
1791
+ project_path = params.get("project_path", "")
1792
+ fragment_type = params.get("fragment_type", "")
1793
+ content = params.get("content", "")
1794
+ if not fragment_type or not content:
1795
+ return {"success": False, "error": "fragment_type and content required"}
1796
+ frag_id = await soul_service.capture_soul_fragment(
1797
+ session_id=sid,
1798
+ fragment_type=fragment_type,
1799
+ content=content,
1800
+ project_path=project_path,
1801
+ )
1802
+ return {"success": frag_id is not None, "fragment_id": frag_id}
1803
+
1804
+ async def _handle_soul_integrate(query, params, session_id):
1805
+ sid = params.get("session_id") or session_id or ""
1806
+ project_path = params.get("project_path", "")
1807
+ result = await soul_service.run_soul_integration(sid, project_path)
1808
+ return {"success": True, **result}
1809
+
1810
+
1775
1811
  # ============================================================
1776
1812
  # SKILL DISPATCH TABLE
1777
1813
  # ============================================================
@@ -1928,6 +1964,11 @@ SKILL_DISPATCH = {
1928
1964
  "session_conflicts": _handle_session_conflicts,
1929
1965
  "session_post_activity": _handle_session_post_activity,
1930
1966
  "session_append_file": _handle_session_append_file,
1967
+
1968
+ # Soul Layer
1969
+ "soul_brief": _handle_soul_brief,
1970
+ "soul_capture": _handle_soul_capture,
1971
+ "soul_integrate": _handle_soul_integrate,
1931
1972
  }
1932
1973
 
1933
1974
 
@@ -4362,17 +4403,41 @@ async def api_mark_insight_applied(insight_id: int):
4362
4403
  # ============= Agent Configuration API =============
4363
4404
 
4364
4405
  @app.get("/api/agents")
4365
- async def get_all_agents():
4366
- """Get all available agents with categories."""
4406
+ async def get_all_agents(project_path: Optional[str] = None):
4407
+ """Get all discovered agents from disk with categories."""
4408
+ agents = discover_agents(project_path)
4409
+ categories = discover_categories(agents)
4410
+ enabled_count = sum(1 for a in agents if a["enabled"])
4367
4411
  return {
4368
4412
  "success": True,
4369
- "agents": AVAILABLE_AGENTS,
4370
- "categories": AGENT_CATEGORIES,
4371
- "by_category": get_agents_by_category(),
4372
- "total": len(AVAILABLE_AGENTS)
4413
+ "agents": agents,
4414
+ "categories": categories,
4415
+ "by_category": get_agents_by_category(agents),
4416
+ "total": len(agents),
4417
+ "enabled": enabled_count,
4373
4418
  }
4374
4419
 
4375
4420
 
4421
+ @app.post("/api/agents/{agent_id}/toggle")
4422
+ async def toggle_agent_endpoint(agent_id: str, request: Request):
4423
+ """Toggle an agent between enabled/disabled by moving files on disk."""
4424
+ try:
4425
+ body = await request.json()
4426
+ enabled = body.get("enabled", False)
4427
+ project_path = body.get("project_path")
4428
+
4429
+ agent = toggle_agent(agent_id, enabled, project_path)
4430
+ return {
4431
+ "success": True,
4432
+ "agent": agent,
4433
+ }
4434
+ except FileNotFoundError as e:
4435
+ return {"success": False, "error": str(e)}
4436
+ except Exception as e:
4437
+ logger.error(f"Error toggling agent {agent_id}: {e}")
4438
+ return {"success": False, "error": str(e)}
4439
+
4440
+
4376
4441
  @app.get("/api/mcps")
4377
4442
  async def get_all_mcps():
4378
4443
  """Get all available MCP servers with live configured status."""
@@ -4435,7 +4500,8 @@ async def get_project_config(project_path: str):
4435
4500
  (project_path,)
4436
4501
  )
4437
4502
 
4438
- # Build agent status map (enabled/disabled)
4503
+ # Build agent status map from disk discovery
4504
+ live_agents = discover_agents(project_path)
4439
4505
  agent_status = {}
4440
4506
  for config in (agent_configs or []):
4441
4507
  agent_status[config['agent_id']] = {
@@ -4444,12 +4510,12 @@ async def get_project_config(project_path: str):
4444
4510
  'settings': json.loads(config['settings']) if config['settings'] else {}
4445
4511
  }
4446
4512
 
4447
- # Fill in defaults for unconfigured agents
4448
- for agent in AVAILABLE_AGENTS:
4513
+ # Fill in defaults for agents not in DB config
4514
+ for agent in live_agents:
4449
4515
  if agent['id'] not in agent_status:
4450
4516
  agent_status[agent['id']] = {
4451
- 'enabled': agent['default_enabled'],
4452
- 'priority': agent['priority'],
4517
+ 'enabled': agent['enabled'],
4518
+ 'priority': 5,
4453
4519
  'settings': {}
4454
4520
  }
4455
4521
 
@@ -4502,7 +4568,7 @@ async def get_project_config(project_path: str):
4502
4568
  "hooks": hook_status,
4503
4569
  "stats": {
4504
4570
  "enabled_agents": sum(1 for a in agent_status.values() if a['enabled']),
4505
- "total_agents": len(AVAILABLE_AGENTS),
4571
+ "total_agents": len(live_agents),
4506
4572
  "enabled_mcps": sum(1 for m in mcp_status.values() if m.get('enabled')),
4507
4573
  "total_mcps": len(live_mcps),
4508
4574
  "configured_mcps": sum(1 for m in mcp_status.values() if m.get('configured')),
@@ -5645,6 +5711,60 @@ async def api_post_session_activity(request: Request):
5645
5711
  return {"success": False, "error": str(e)}
5646
5712
 
5647
5713
 
5714
+ # ============= Soul Layer REST Endpoints =============
5715
+
5716
+
5717
+ @app.get("/api/soul/brief")
5718
+ async def api_soul_brief(project_path: str = ""):
5719
+ """Get the soul brief for a project."""
5720
+ try:
5721
+ brief = await soul_service.generate_soul_brief(project_path)
5722
+ return {"success": True, "brief": brief}
5723
+ except Exception as e:
5724
+ logger.error(f"Soul brief failed: {e}")
5725
+ return {"success": False, "error": str(e), "brief": ""}
5726
+
5727
+
5728
+ @app.post("/api/soul/capture")
5729
+ async def api_soul_capture(request: Request):
5730
+ """Capture a soul fragment from a session response."""
5731
+ try:
5732
+ body = await request.json()
5733
+ session_id = body.get("session_id", "")
5734
+ project_path = body.get("project_path", "")
5735
+ fragment_type = body.get("fragment_type", "")
5736
+ content = body.get("content", "")
5737
+
5738
+ if not fragment_type or not content:
5739
+ return {"success": False, "error": "fragment_type and content required"}
5740
+
5741
+ frag_id = await soul_service.capture_soul_fragment(
5742
+ session_id=session_id,
5743
+ fragment_type=fragment_type,
5744
+ content=content,
5745
+ project_path=project_path,
5746
+ )
5747
+ return {"success": frag_id is not None, "fragment_id": frag_id}
5748
+ except Exception as e:
5749
+ logger.error(f"Soul capture failed: {e}")
5750
+ return {"success": False, "error": str(e)}
5751
+
5752
+
5753
+ @app.post("/api/soul/integrate")
5754
+ async def api_soul_integrate(request: Request):
5755
+ """Run soul integration for a session — merges fragments into soul_state."""
5756
+ try:
5757
+ body = await request.json()
5758
+ session_id = body.get("session_id", "")
5759
+ project_path = body.get("project_path", "")
5760
+
5761
+ result = await soul_service.run_soul_integration(session_id, project_path)
5762
+ return {"success": True, **result}
5763
+ except Exception as e:
5764
+ logger.error(f"Soul integration failed: {e}")
5765
+ return {"success": False, "error": str(e)}
5766
+
5767
+
5648
5768
  # ============= Aggregated Grounding Context (v2) =============
5649
5769
 
5650
5770
 
package/mcp_proxy.py CHANGED
@@ -180,6 +180,12 @@ async def memory_ask(
180
180
  elif label == "sessions":
181
181
  results["active_sessions"] = result.get("sessions", [])
182
182
 
183
+ # -- Soul context enrichment (lightweight DB read) --
184
+ if project_path:
185
+ soul_data = await _rest_get("/api/soul/brief", {"project_path": project_path})
186
+ if soul_data and soul_data.get("brief"):
187
+ results["soul_context"] = {"brief": soul_data["brief"]}
188
+
183
189
  results["success"] = bool(results.get("memories") or results.get("patterns"))
184
190
  return json.dumps(results, default=str)
185
191
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-memory-agent",
3
- "version": "3.0.2",
4
- "description": "Persistent semantic memory system for Claude Code sessions with anti-hallucination grounding",
3
+ "version": "3.1.0",
4
+ "description": "Persistent semantic memory for Claude Code with soul layer, personality learning, and anti-hallucination grounding",
5
5
  "keywords": [
6
6
  "claude",
7
7
  "claude-code",