superlocalmemory 3.0.37 → 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/package.json +1 -1
- package/pyproject.toml +2 -1
- package/src/superlocalmemory/cli/commands.py +96 -0
- package/src/superlocalmemory/cli/main.py +13 -0
- package/src/superlocalmemory/core/engine.py +63 -0
- package/src/superlocalmemory/core/summarizer.py +4 -26
- package/src/superlocalmemory/hooks/claude_code_hooks.py +175 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +289 -0
- package/src/superlocalmemory/learning/signals.py +326 -0
- package/src/superlocalmemory/llm/backbone.py +14 -5
- package/src/superlocalmemory/mcp/resources.py +26 -1
- package/src/superlocalmemory/mcp/server.py +2 -0
- package/src/superlocalmemory/mcp/tools_active.py +205 -0
- package/src/superlocalmemory/mcp/tools_core.py +51 -0
- package/src/superlocalmemory/server/routes/behavioral.py +20 -5
- package/src/superlocalmemory/server/routes/learning.py +69 -12
- package/src/superlocalmemory/server/routes/stats.py +33 -5
- package/src/superlocalmemory/server/routes/v3_api.py +93 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "superlocalmemory"
|
|
3
|
-
version = "3.0
|
|
3
|
+
version = "3.1.0"
|
|
4
4
|
description = "Information-geometric agent memory with mathematical guarantees"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -26,6 +26,7 @@ dependencies = [
|
|
|
26
26
|
"fastapi[all]>=0.135.1",
|
|
27
27
|
"uvicorn>=0.42.0",
|
|
28
28
|
"websockets>=16.0",
|
|
29
|
+
"lightgbm>=4.0.0",
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
[project.optional-dependencies]
|
|
@@ -37,6 +37,9 @@ def dispatch(args: Namespace) -> None:
|
|
|
37
37
|
"warmup": cmd_warmup,
|
|
38
38
|
"dashboard": cmd_dashboard,
|
|
39
39
|
"profile": cmd_profile,
|
|
40
|
+
"hooks": cmd_hooks,
|
|
41
|
+
"session-context": cmd_session_context,
|
|
42
|
+
"observe": cmd_observe,
|
|
40
43
|
}
|
|
41
44
|
handler = handlers.get(args.command)
|
|
42
45
|
if handler:
|
|
@@ -763,3 +766,96 @@ def cmd_profile(args: Namespace) -> None:
|
|
|
763
766
|
)
|
|
764
767
|
ensure_profile_in_json(args.name)
|
|
765
768
|
print(f"Created profile: {args.name}")
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
# -- Active Memory commands (V3.1) ------------------------------------------
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def cmd_hooks(args: Namespace) -> None:
|
|
775
|
+
"""Manage Claude Code hooks for invisible memory injection."""
|
|
776
|
+
from superlocalmemory.hooks.claude_code_hooks import (
|
|
777
|
+
install_hooks, remove_hooks, check_status,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
action = getattr(args, "action", "status")
|
|
781
|
+
if action == "install":
|
|
782
|
+
result = install_hooks()
|
|
783
|
+
if result["scripts"] and result["settings"]:
|
|
784
|
+
print("SLM hooks installed in Claude Code.")
|
|
785
|
+
print("Memory context will auto-inject on every new session.")
|
|
786
|
+
else:
|
|
787
|
+
print(f"Installation incomplete: {result['errors']}")
|
|
788
|
+
elif action == "remove":
|
|
789
|
+
result = remove_hooks()
|
|
790
|
+
if result["scripts"] and result["settings"]:
|
|
791
|
+
print("SLM hooks removed from Claude Code.")
|
|
792
|
+
else:
|
|
793
|
+
print(f"Removal incomplete: {result['errors']}")
|
|
794
|
+
else:
|
|
795
|
+
result = check_status()
|
|
796
|
+
if result["installed"]:
|
|
797
|
+
print("SLM hooks: INSTALLED")
|
|
798
|
+
print(f" Scripts: {result['hooks_dir']}")
|
|
799
|
+
print(" Claude Code settings: configured")
|
|
800
|
+
else:
|
|
801
|
+
print("SLM hooks: NOT INSTALLED")
|
|
802
|
+
print(" Run: slm hooks install")
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def cmd_session_context(args: Namespace) -> None:
|
|
806
|
+
"""Print session context (for hook scripts and piping)."""
|
|
807
|
+
from superlocalmemory.hooks.auto_recall import AutoRecall
|
|
808
|
+
from superlocalmemory.core.config import SLMConfig
|
|
809
|
+
from superlocalmemory.core.engine import MemoryEngine
|
|
810
|
+
|
|
811
|
+
try:
|
|
812
|
+
config = SLMConfig.load()
|
|
813
|
+
engine = MemoryEngine(config)
|
|
814
|
+
engine.initialize()
|
|
815
|
+
|
|
816
|
+
auto = AutoRecall(
|
|
817
|
+
engine=engine,
|
|
818
|
+
config={"enabled": True, "max_memories_injected": 10, "relevance_threshold": 0.3},
|
|
819
|
+
)
|
|
820
|
+
context = auto.get_session_context(
|
|
821
|
+
query=getattr(args, "query", "") or "recent decisions and important context",
|
|
822
|
+
)
|
|
823
|
+
if context:
|
|
824
|
+
print(context)
|
|
825
|
+
except Exception as exc:
|
|
826
|
+
logger.debug("session-context failed: %s", exc)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def cmd_observe(args: Namespace) -> None:
|
|
830
|
+
"""Evaluate and auto-capture content from stdin or argument."""
|
|
831
|
+
import sys
|
|
832
|
+
from superlocalmemory.hooks.auto_capture import AutoCapture
|
|
833
|
+
from superlocalmemory.core.config import SLMConfig
|
|
834
|
+
from superlocalmemory.core.engine import MemoryEngine
|
|
835
|
+
|
|
836
|
+
content = getattr(args, "content", "") or ""
|
|
837
|
+
if not content and not sys.stdin.isatty():
|
|
838
|
+
content = sys.stdin.read().strip()
|
|
839
|
+
|
|
840
|
+
if not content:
|
|
841
|
+
print("No content to observe.")
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
try:
|
|
845
|
+
config = SLMConfig.load()
|
|
846
|
+
engine = MemoryEngine(config)
|
|
847
|
+
engine.initialize()
|
|
848
|
+
|
|
849
|
+
auto = AutoCapture(engine=engine)
|
|
850
|
+
decision = auto.evaluate(content)
|
|
851
|
+
|
|
852
|
+
if decision.capture:
|
|
853
|
+
stored = auto.capture(content, category=decision.category)
|
|
854
|
+
if stored:
|
|
855
|
+
print(f"Auto-captured: {decision.category} (confidence: {decision.confidence:.2f})")
|
|
856
|
+
else:
|
|
857
|
+
print(f"Detected {decision.category} but store failed.")
|
|
858
|
+
else:
|
|
859
|
+
print(f"Not captured: {decision.reason}")
|
|
860
|
+
except Exception as exc:
|
|
861
|
+
logger.debug("observe failed: %s", exc)
|
|
@@ -170,6 +170,19 @@ def main() -> None:
|
|
|
170
170
|
profile_p.add_argument("name", nargs="?", help="Profile name")
|
|
171
171
|
profile_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
|
|
172
172
|
|
|
173
|
+
# -- Active Memory (V3.1) ------------------------------------------
|
|
174
|
+
hooks_p = sub.add_parser("hooks", help="Manage Claude Code hooks for auto memory injection")
|
|
175
|
+
hooks_p.add_argument(
|
|
176
|
+
"action", nargs="?", default="status",
|
|
177
|
+
choices=["install", "remove", "status"], help="Action (default: status)",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
ctx_p = sub.add_parser("session-context", help="Print session context (for hooks)")
|
|
181
|
+
ctx_p.add_argument("query", nargs="?", default="", help="Optional context query")
|
|
182
|
+
|
|
183
|
+
obs_p = sub.add_parser("observe", help="Auto-capture content (pipe or argument)")
|
|
184
|
+
obs_p.add_argument("content", nargs="?", default="", help="Content to evaluate")
|
|
185
|
+
|
|
173
186
|
args = parser.parse_args()
|
|
174
187
|
|
|
175
188
|
if not args.command:
|
|
@@ -460,6 +460,15 @@ class MemoryEngine:
|
|
|
460
460
|
except Exception as exc:
|
|
461
461
|
logger.debug("Agentic sufficiency skipped: %s", exc)
|
|
462
462
|
|
|
463
|
+
# Adaptive re-ranking (V3.1 Active Memory)
|
|
464
|
+
# Phase 1 (< 50 signals): no change (cross-encoder order preserved)
|
|
465
|
+
# Phase 2 (50+): heuristic boosts (recency, access, trust)
|
|
466
|
+
# Phase 3 (200+): LightGBM ML ranking
|
|
467
|
+
try:
|
|
468
|
+
response = self._apply_adaptive_ranking(response, query, pid)
|
|
469
|
+
except Exception as exc:
|
|
470
|
+
logger.debug("Adaptive ranking skipped: %s", exc)
|
|
471
|
+
|
|
463
472
|
# Reconsolidation: access updates trust + count (neuroscience principle)
|
|
464
473
|
if self._trust_scorer:
|
|
465
474
|
for r in response.results:
|
|
@@ -614,6 +623,60 @@ class MemoryEngine:
|
|
|
614
623
|
if not self._initialized:
|
|
615
624
|
self.initialize()
|
|
616
625
|
|
|
626
|
+
def _apply_adaptive_ranking(self, response, query: str, pid: str):
|
|
627
|
+
"""Apply adaptive re-ranking if enough learning signals exist.
|
|
628
|
+
|
|
629
|
+
Phase 1 (< 50 signals): returns response unchanged (backward compat).
|
|
630
|
+
Phase 2 (50+): heuristic boosts from recency, access count, trust.
|
|
631
|
+
Phase 3 (200+): LightGBM ML-based reranking.
|
|
632
|
+
"""
|
|
633
|
+
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
634
|
+
from pathlib import Path
|
|
635
|
+
|
|
636
|
+
learning_db = Path.home() / ".superlocalmemory" / "learning.db"
|
|
637
|
+
if not learning_db.exists():
|
|
638
|
+
return response
|
|
639
|
+
|
|
640
|
+
collector = FeedbackCollector(learning_db)
|
|
641
|
+
signal_count = collector.get_feedback_count(pid)
|
|
642
|
+
|
|
643
|
+
if signal_count < 50:
|
|
644
|
+
return response # Phase 1: no change
|
|
645
|
+
|
|
646
|
+
from superlocalmemory.learning.ranker import AdaptiveRanker
|
|
647
|
+
ranker = AdaptiveRanker(signal_count=signal_count)
|
|
648
|
+
|
|
649
|
+
result_dicts = []
|
|
650
|
+
for r in response.results:
|
|
651
|
+
result_dicts.append({
|
|
652
|
+
"score": r.score,
|
|
653
|
+
"cross_encoder_score": r.score,
|
|
654
|
+
"trust_score": r.trust_score,
|
|
655
|
+
"channel_scores": r.channel_scores or {},
|
|
656
|
+
"fact": {
|
|
657
|
+
"age_days": 0,
|
|
658
|
+
"access_count": r.fact.access_count,
|
|
659
|
+
},
|
|
660
|
+
"_original": r,
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
query_context = {"query_type": response.query_type}
|
|
664
|
+
reranked = ranker.rerank(result_dicts, query_context)
|
|
665
|
+
|
|
666
|
+
# Rebuild response with new ordering
|
|
667
|
+
new_results = [d["_original"] for d in reranked]
|
|
668
|
+
|
|
669
|
+
from superlocalmemory.storage.models import RecallResponse
|
|
670
|
+
return RecallResponse(
|
|
671
|
+
query=response.query,
|
|
672
|
+
mode=response.mode,
|
|
673
|
+
results=new_results,
|
|
674
|
+
query_type=response.query_type,
|
|
675
|
+
channel_weights=response.channel_weights,
|
|
676
|
+
total_candidates=response.total_candidates,
|
|
677
|
+
retrieval_time_ms=response.retrieval_time_ms,
|
|
678
|
+
)
|
|
679
|
+
|
|
617
680
|
def _init_encoding(self) -> None:
|
|
618
681
|
from superlocalmemory.encoding.fact_extractor import FactExtractor
|
|
619
682
|
from superlocalmemory.encoding.entity_resolver import EntityResolver
|
|
@@ -94,14 +94,13 @@ class Summarizer:
|
|
|
94
94
|
# ------------------------------------------------------------------
|
|
95
95
|
|
|
96
96
|
def _has_llm(self) -> bool:
|
|
97
|
-
"""Check if LLM is available
|
|
97
|
+
"""Check if LLM is available.
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
spike 5+ GB of RAM on every recall, unacceptable on ≤32 GB machines.
|
|
99
|
+
Mode B: Ollama assumed running (num_ctx: 4096 caps memory at 5.5 GB).
|
|
100
|
+
Mode C: Requires API key for cloud provider.
|
|
102
101
|
"""
|
|
103
102
|
if self._mode == "b":
|
|
104
|
-
return
|
|
103
|
+
return True
|
|
105
104
|
if self._mode == "c":
|
|
106
105
|
return bool(
|
|
107
106
|
os.environ.get("OPENROUTER_API_KEY")
|
|
@@ -109,27 +108,6 @@ class Summarizer:
|
|
|
109
108
|
)
|
|
110
109
|
return False
|
|
111
110
|
|
|
112
|
-
def _is_ollama_model_warm(self) -> bool:
|
|
113
|
-
"""Check if the LLM model is already loaded in Ollama memory.
|
|
114
|
-
|
|
115
|
-
Queries Ollama /api/ps. Returns True only if our model is loaded,
|
|
116
|
-
preventing cold-load memory spikes during recall.
|
|
117
|
-
"""
|
|
118
|
-
try:
|
|
119
|
-
import httpx
|
|
120
|
-
model = getattr(self._config.llm, 'model', None) or "llama3.1:8b"
|
|
121
|
-
model_base = model.split(":")[0]
|
|
122
|
-
with httpx.Client(timeout=httpx.Timeout(2.0)) as client:
|
|
123
|
-
resp = client.get("http://localhost:11434/api/ps")
|
|
124
|
-
if resp.status_code != 200:
|
|
125
|
-
return False
|
|
126
|
-
for m in resp.json().get("models", []):
|
|
127
|
-
if model_base in m.get("name", ""):
|
|
128
|
-
return True
|
|
129
|
-
return False
|
|
130
|
-
except Exception:
|
|
131
|
-
return False
|
|
132
|
-
|
|
133
111
|
def _call_llm(self, prompt: str, max_tokens: int = 200) -> str:
|
|
134
112
|
"""Route to Ollama (B) or OpenRouter (C)."""
|
|
135
113
|
if self._mode == "b":
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Claude Code Hook Integration — invisible memory injection.
|
|
6
|
+
|
|
7
|
+
Installs hooks into Claude Code's settings.json that auto-inject
|
|
8
|
+
SLM context on session start and auto-capture on tool use.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
slm hooks install Install hooks into Claude Code settings
|
|
12
|
+
slm hooks status Check if hooks are installed
|
|
13
|
+
slm hooks remove Remove SLM hooks from settings
|
|
14
|
+
|
|
15
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import shutil
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
|
|
28
|
+
HOOKS_DIR = Path.home() / ".superlocalmemory" / "hooks"
|
|
29
|
+
|
|
30
|
+
# The hook scripts that Claude Code will execute
|
|
31
|
+
HOOK_SCRIPTS = {
|
|
32
|
+
"slm-session-start.sh": """\
|
|
33
|
+
#!/bin/bash
|
|
34
|
+
# SLM Active Memory — Session Start Hook
|
|
35
|
+
# Auto-recalls relevant context at session start
|
|
36
|
+
slm session-context 2>/dev/null || true
|
|
37
|
+
""",
|
|
38
|
+
"slm-auto-capture.sh": """\
|
|
39
|
+
#!/bin/bash
|
|
40
|
+
# SLM Active Memory — Auto-Capture Hook
|
|
41
|
+
# Evaluates tool output for decisions/bugs/preferences
|
|
42
|
+
# Input comes via stdin from Claude Code PostToolUse event
|
|
43
|
+
INPUT=$(cat)
|
|
44
|
+
if [ -n "$INPUT" ]; then
|
|
45
|
+
echo "$INPUT" | slm observe 2>/dev/null || true
|
|
46
|
+
fi
|
|
47
|
+
""",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Hook definitions for Claude Code settings.json
|
|
51
|
+
HOOK_DEFINITIONS = {
|
|
52
|
+
"hooks": {
|
|
53
|
+
"SessionStart": [
|
|
54
|
+
{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": str(HOOKS_DIR / "slm-session-start.sh"),
|
|
57
|
+
"timeout": 10000,
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def install_hooks() -> dict:
|
|
65
|
+
"""Install SLM hooks into Claude Code settings."""
|
|
66
|
+
results = {"scripts": False, "settings": False, "errors": []}
|
|
67
|
+
|
|
68
|
+
# 1. Create hook scripts
|
|
69
|
+
try:
|
|
70
|
+
HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
for name, content in HOOK_SCRIPTS.items():
|
|
72
|
+
path = HOOKS_DIR / name
|
|
73
|
+
path.write_text(content)
|
|
74
|
+
path.chmod(0o755)
|
|
75
|
+
results["scripts"] = True
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
results["errors"].append(f"Script creation failed: {exc}")
|
|
78
|
+
|
|
79
|
+
# 2. Update Claude Code settings.json
|
|
80
|
+
try:
|
|
81
|
+
if not CLAUDE_SETTINGS.parent.exists():
|
|
82
|
+
CLAUDE_SETTINGS.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
settings = {}
|
|
85
|
+
if CLAUDE_SETTINGS.exists():
|
|
86
|
+
settings = json.loads(CLAUDE_SETTINGS.read_text())
|
|
87
|
+
|
|
88
|
+
# Merge hooks without overwriting existing ones
|
|
89
|
+
if "hooks" not in settings:
|
|
90
|
+
settings["hooks"] = {}
|
|
91
|
+
|
|
92
|
+
# Add SessionStart hook if not present
|
|
93
|
+
session_hooks = settings["hooks"].get("SessionStart", [])
|
|
94
|
+
slm_hook_cmd = str(HOOKS_DIR / "slm-session-start.sh")
|
|
95
|
+
already_installed = any(
|
|
96
|
+
h.get("command", "") == slm_hook_cmd
|
|
97
|
+
for h in session_hooks if isinstance(h, dict)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not already_installed:
|
|
101
|
+
session_hooks.append({
|
|
102
|
+
"type": "command",
|
|
103
|
+
"command": slm_hook_cmd,
|
|
104
|
+
"timeout": 10000,
|
|
105
|
+
})
|
|
106
|
+
settings["hooks"]["SessionStart"] = session_hooks
|
|
107
|
+
|
|
108
|
+
CLAUDE_SETTINGS.write_text(json.dumps(settings, indent=2))
|
|
109
|
+
results["settings"] = True
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
results["errors"].append(f"Settings update failed: {exc}")
|
|
112
|
+
|
|
113
|
+
return results
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def remove_hooks() -> dict:
|
|
117
|
+
"""Remove SLM hooks from Claude Code settings."""
|
|
118
|
+
results = {"scripts": False, "settings": False, "errors": []}
|
|
119
|
+
|
|
120
|
+
# 1. Remove hook scripts
|
|
121
|
+
try:
|
|
122
|
+
if HOOKS_DIR.exists():
|
|
123
|
+
shutil.rmtree(HOOKS_DIR)
|
|
124
|
+
results["scripts"] = True
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
results["errors"].append(f"Script removal failed: {exc}")
|
|
127
|
+
|
|
128
|
+
# 2. Remove from Claude Code settings
|
|
129
|
+
try:
|
|
130
|
+
if CLAUDE_SETTINGS.exists():
|
|
131
|
+
settings = json.loads(CLAUDE_SETTINGS.read_text())
|
|
132
|
+
if "hooks" in settings and "SessionStart" in settings["hooks"]:
|
|
133
|
+
slm_hook_cmd = str(HOOKS_DIR / "slm-session-start.sh")
|
|
134
|
+
settings["hooks"]["SessionStart"] = [
|
|
135
|
+
h for h in settings["hooks"]["SessionStart"]
|
|
136
|
+
if not (isinstance(h, dict) and h.get("command", "") == slm_hook_cmd)
|
|
137
|
+
]
|
|
138
|
+
if not settings["hooks"]["SessionStart"]:
|
|
139
|
+
del settings["hooks"]["SessionStart"]
|
|
140
|
+
if not settings["hooks"]:
|
|
141
|
+
del settings["hooks"]
|
|
142
|
+
CLAUDE_SETTINGS.write_text(json.dumps(settings, indent=2))
|
|
143
|
+
results["settings"] = True
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
results["errors"].append(f"Settings cleanup failed: {exc}")
|
|
146
|
+
|
|
147
|
+
return results
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def check_status() -> dict:
|
|
151
|
+
"""Check if SLM hooks are installed."""
|
|
152
|
+
scripts_ok = all(
|
|
153
|
+
(HOOKS_DIR / name).exists()
|
|
154
|
+
for name in HOOK_SCRIPTS
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
settings_ok = False
|
|
158
|
+
try:
|
|
159
|
+
if CLAUDE_SETTINGS.exists():
|
|
160
|
+
settings = json.loads(CLAUDE_SETTINGS.read_text())
|
|
161
|
+
session_hooks = settings.get("hooks", {}).get("SessionStart", [])
|
|
162
|
+
slm_hook_cmd = str(HOOKS_DIR / "slm-session-start.sh")
|
|
163
|
+
settings_ok = any(
|
|
164
|
+
h.get("command", "") == slm_hook_cmd
|
|
165
|
+
for h in session_hooks if isinstance(h, dict)
|
|
166
|
+
)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
"installed": scripts_ok and settings_ok,
|
|
172
|
+
"scripts": scripts_ok,
|
|
173
|
+
"settings": settings_ok,
|
|
174
|
+
"hooks_dir": str(HOOKS_DIR),
|
|
175
|
+
}
|