superlocalmemory 3.3.5 → 3.3.7
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/README.md +2 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/postinstall.js +32 -1
- package/src/superlocalmemory/cli/commands.py +129 -9
- package/src/superlocalmemory/cli/main.py +19 -0
- package/src/superlocalmemory/core/embedding_worker.py +27 -1
- package/src/superlocalmemory/core/embeddings.py +39 -0
- package/src/superlocalmemory/core/recall_worker.py +26 -0
- package/src/superlocalmemory/hooks/claude_code_hooks.py +351 -122
- package/src/superlocalmemory/hooks/hook_handlers.py +394 -0
- package/src/superlocalmemory/retrieval/reranker.py +39 -0
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
# Licensed under the MIT License - see LICENSE file
|
|
3
3
|
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
4
|
|
|
5
|
-
"""Claude Code
|
|
5
|
+
"""Claude Code hook integration — hybrid approach (v3.3.6).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
CRITICAL PATH (gate + init-done): Shell built-ins only. Cannot crash.
|
|
8
|
+
VALUE-ADD (start, checkpoint, stop): Python via `slm hook <name>`,
|
|
9
|
+
wrapped with `2>/dev/null || true` so errors are invisible.
|
|
9
10
|
|
|
10
11
|
Usage:
|
|
11
|
-
slm hooks install
|
|
12
|
-
slm hooks
|
|
13
|
-
slm hooks
|
|
12
|
+
slm hooks install Install all hooks into Claude Code
|
|
13
|
+
slm hooks remove Remove SLM hooks from Claude Code
|
|
14
|
+
slm hooks status Check installation status
|
|
15
|
+
slm init Full setup including hooks
|
|
14
16
|
|
|
15
17
|
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
16
18
|
"""
|
|
@@ -19,157 +21,384 @@ from __future__ import annotations
|
|
|
19
21
|
|
|
20
22
|
import json
|
|
21
23
|
import logging
|
|
22
|
-
import
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
23
26
|
from pathlib import Path
|
|
24
27
|
|
|
25
28
|
logger = logging.getLogger(__name__)
|
|
26
29
|
|
|
27
30
|
CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
slm
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
31
|
+
VERSION_DIR = Path.home() / ".superlocalmemory" / "hooks"
|
|
32
|
+
VERSION_FILE = VERSION_DIR / ".version"
|
|
33
|
+
DISABLED_FILE = VERSION_DIR / ".hooks-disabled"
|
|
34
|
+
HOOKS_VERSION = "3.3.6"
|
|
35
|
+
|
|
36
|
+
# Cross-platform temp dir and marker paths
|
|
37
|
+
_TMP = tempfile.gettempdir()
|
|
38
|
+
_MARKER = f"{_TMP}/slm-session-initialized"
|
|
39
|
+
_START_MARKER = f"{_TMP}/slm-session-start-time"
|
|
40
|
+
|
|
41
|
+
# Tools that the gate should block (everything except SLM/ToolSearch)
|
|
42
|
+
_GATED_TOOLS = "Bash|Read|Write|Edit|Glob|Grep|Agent|WebFetch|WebSearch|NotebookEdit"
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Platform-specific gate commands (shell built-ins only — CANNOT crash)
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def _gate_cmd() -> str:
|
|
49
|
+
"""Gate command: pure shell, no Python, ~1ms.
|
|
50
|
+
|
|
51
|
+
Logic: if initialized → allow. If no session started → allow. Else → block.
|
|
52
|
+
Uses specific matcher to exclude SLM tools, so no stdin parsing needed.
|
|
53
|
+
"""
|
|
54
|
+
if sys.platform == "win32":
|
|
55
|
+
marker_win = _MARKER.replace("/", "\\")
|
|
56
|
+
start_win = _START_MARKER.replace("/", "\\")
|
|
57
|
+
return (
|
|
58
|
+
f'cmd /c "if exist {marker_win} (exit /b 0)'
|
|
59
|
+
f' else if not exist {start_win} (exit /b 0)'
|
|
60
|
+
f' else (echo [SLM] Call mcp__superlocalmemory__session_init first & exit /b 2)"'
|
|
61
|
+
)
|
|
62
|
+
return (
|
|
63
|
+
f"test -f {_MARKER}"
|
|
64
|
+
f" || test ! -f {_START_MARKER}"
|
|
65
|
+
" || { echo '[SLM] Call mcp__superlocalmemory__session_init first'; exit 2; }"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _init_done_cmd() -> str:
|
|
70
|
+
"""Init-done command: pure shell touch, ~1ms."""
|
|
71
|
+
if sys.platform == "win32":
|
|
72
|
+
return f'cmd /c "echo.>{_MARKER.replace("/", chr(92))}"'
|
|
73
|
+
return f"touch {_MARKER}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _wrap_python_cmd(hook_name: str) -> str:
|
|
77
|
+
"""Wrap a Python hook with error absorption. Any crash → invisible."""
|
|
78
|
+
if sys.platform == "win32":
|
|
79
|
+
return f'cmd /c "slm hook {hook_name} 2>NUL || exit /b 0"'
|
|
80
|
+
return f"slm hook {hook_name} 2>/dev/null || true"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Hook definitions for settings.json
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def _hook_definitions(include_gate: bool = False) -> dict[str, list]:
|
|
88
|
+
"""Build Claude Code hook entries.
|
|
89
|
+
|
|
90
|
+
Critical path (gate, init-done): Shell built-ins. Cannot crash.
|
|
91
|
+
Value-add (start, checkpoint, stop): Python with error wrapper.
|
|
92
|
+
"""
|
|
93
|
+
defs: dict[str, list] = {
|
|
53
94
|
"SessionStart": [
|
|
54
95
|
{
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
96
|
+
"hooks": [
|
|
97
|
+
{
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": _wrap_python_cmd("start"),
|
|
100
|
+
"timeout": 15000,
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"PostToolUse": [
|
|
106
|
+
{
|
|
107
|
+
"matcher": "Write|Edit",
|
|
108
|
+
"hooks": [
|
|
109
|
+
{
|
|
110
|
+
"type": "command",
|
|
111
|
+
"command": _wrap_python_cmd("checkpoint"),
|
|
112
|
+
"timeout": 5000,
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"Stop": [
|
|
118
|
+
{
|
|
119
|
+
"hooks": [
|
|
120
|
+
{
|
|
121
|
+
"type": "command",
|
|
122
|
+
"command": _wrap_python_cmd("stop"),
|
|
123
|
+
"timeout": 10000,
|
|
124
|
+
}
|
|
125
|
+
]
|
|
58
126
|
}
|
|
59
127
|
],
|
|
60
128
|
}
|
|
61
|
-
}
|
|
62
129
|
|
|
130
|
+
if include_gate:
|
|
131
|
+
defs["PreToolUse"] = [
|
|
132
|
+
{
|
|
133
|
+
"matcher": _GATED_TOOLS,
|
|
134
|
+
"hooks": [
|
|
135
|
+
{
|
|
136
|
+
"type": "command",
|
|
137
|
+
"command": _gate_cmd(),
|
|
138
|
+
"timeout": 500,
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
defs["PostToolUse"].insert(0, {
|
|
144
|
+
"matcher": "mcp__superlocalmemory__session_init",
|
|
145
|
+
"hooks": [
|
|
146
|
+
{
|
|
147
|
+
"type": "command",
|
|
148
|
+
"command": _init_done_cmd(),
|
|
149
|
+
"timeout": 500,
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return defs
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Identify SLM hooks in existing settings
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def _is_slm_hook_entry(entry: dict) -> bool:
|
|
162
|
+
"""Check if a hook entry belongs to SLM."""
|
|
163
|
+
for hook in entry.get("hooks", []):
|
|
164
|
+
cmd = hook.get("command", "")
|
|
165
|
+
if ("slm hook" in cmd
|
|
166
|
+
or "slm-session" in cmd
|
|
167
|
+
or ".superlocalmemory/hooks/" in cmd
|
|
168
|
+
or "slm-session-initialized" in cmd):
|
|
169
|
+
return True
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Safe settings.json merge / removal
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def _merge_hooks(settings: dict, hook_defs: dict) -> dict:
|
|
178
|
+
"""Merge SLM hooks into settings, preserving all non-SLM hooks."""
|
|
179
|
+
if "hooks" not in settings:
|
|
180
|
+
settings["hooks"] = {}
|
|
181
|
+
|
|
182
|
+
for hook_type, slm_entries in hook_defs.items():
|
|
183
|
+
existing = settings["hooks"].get(hook_type, [])
|
|
184
|
+
cleaned = [e for e in existing if not _is_slm_hook_entry(e)]
|
|
185
|
+
cleaned.extend(slm_entries)
|
|
186
|
+
settings["hooks"][hook_type] = cleaned
|
|
187
|
+
|
|
188
|
+
return settings
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _remove_slm_hooks(settings: dict) -> dict:
|
|
192
|
+
"""Remove all SLM hook entries, preserve non-SLM hooks."""
|
|
193
|
+
hooks = settings.get("hooks", {})
|
|
194
|
+
for hook_type in list(hooks.keys()):
|
|
195
|
+
cleaned = [e for e in hooks[hook_type] if not _is_slm_hook_entry(e)]
|
|
196
|
+
if cleaned:
|
|
197
|
+
hooks[hook_type] = cleaned
|
|
198
|
+
else:
|
|
199
|
+
del hooks[hook_type]
|
|
200
|
+
if not hooks and "hooks" in settings:
|
|
201
|
+
del settings["hooks"]
|
|
202
|
+
return settings
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _read_settings() -> dict:
|
|
206
|
+
"""Read Claude Code settings.json, return empty dict if missing."""
|
|
207
|
+
if CLAUDE_SETTINGS.exists():
|
|
208
|
+
return json.loads(CLAUDE_SETTINGS.read_text())
|
|
209
|
+
return {}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _write_settings(settings: dict) -> None:
|
|
213
|
+
"""Write settings.json with pretty formatting."""
|
|
214
|
+
CLAUDE_SETTINGS.parent.mkdir(parents=True, exist_ok=True)
|
|
215
|
+
CLAUDE_SETTINGS.write_text(json.dumps(settings, indent=2) + "\n")
|
|
63
216
|
|
|
64
|
-
def install_hooks() -> dict:
|
|
65
|
-
"""Install SLM hooks into Claude Code settings."""
|
|
66
|
-
results = {"scripts": False, "settings": False, "errors": []}
|
|
67
217
|
|
|
68
|
-
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
# Public API
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
def install_hooks(include_gate: bool = False) -> dict:
|
|
223
|
+
"""Install SLM hooks into Claude Code settings.json.
|
|
224
|
+
|
|
225
|
+
Critical path uses shell built-ins (cannot crash).
|
|
226
|
+
Value-add uses Python with error wrappers (crashes invisible).
|
|
227
|
+
Never overwrites non-SLM hooks.
|
|
228
|
+
Clears .hooks-disabled marker (explicit install = user wants hooks).
|
|
229
|
+
"""
|
|
230
|
+
result = {
|
|
231
|
+
"success": False, "errors": [],
|
|
232
|
+
"hooks_added": [], "gate_enabled": include_gate,
|
|
233
|
+
}
|
|
234
|
+
|
|
69
235
|
try:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
236
|
+
settings = _read_settings()
|
|
237
|
+
hook_defs = _hook_definitions(include_gate=include_gate)
|
|
238
|
+
settings = _merge_hooks(settings, hook_defs)
|
|
239
|
+
_write_settings(settings)
|
|
240
|
+
result["hooks_added"] = list(hook_defs.keys())
|
|
241
|
+
result["success"] = True
|
|
76
242
|
except Exception as exc:
|
|
77
|
-
|
|
243
|
+
result["errors"].append(f"Settings update failed: {exc}")
|
|
78
244
|
|
|
79
|
-
# 2. Update Claude Code settings.json
|
|
80
245
|
try:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
246
|
+
VERSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
VERSION_FILE.write_text(HOOKS_VERSION)
|
|
248
|
+
# Clear disabled marker — explicit install means user wants hooks
|
|
249
|
+
if DISABLED_FILE.exists():
|
|
250
|
+
DISABLED_FILE.unlink()
|
|
110
251
|
except Exception as exc:
|
|
111
|
-
|
|
252
|
+
result["errors"].append(f"Version file failed: {exc}")
|
|
112
253
|
|
|
113
|
-
return
|
|
254
|
+
return result
|
|
114
255
|
|
|
115
256
|
|
|
116
257
|
def remove_hooks() -> dict:
|
|
117
|
-
"""Remove SLM hooks from Claude Code settings.
|
|
118
|
-
|
|
258
|
+
"""Remove all SLM hooks from Claude Code settings.json.
|
|
259
|
+
|
|
260
|
+
Writes a .hooks-disabled marker so auto-install paths respect
|
|
261
|
+
the user's explicit choice. Cleared by explicit `install_hooks()`.
|
|
262
|
+
"""
|
|
263
|
+
result = {"success": False, "errors": []}
|
|
119
264
|
|
|
120
|
-
# 1. Remove hook scripts
|
|
121
265
|
try:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
266
|
+
settings = _read_settings()
|
|
267
|
+
settings = _remove_slm_hooks(settings)
|
|
268
|
+
_write_settings(settings)
|
|
269
|
+
result["success"] = True
|
|
125
270
|
except Exception as exc:
|
|
126
|
-
|
|
271
|
+
result["errors"].append(f"Settings cleanup failed: {exc}")
|
|
127
272
|
|
|
128
|
-
# 2. Remove from Claude Code settings
|
|
129
273
|
try:
|
|
130
|
-
if
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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}")
|
|
274
|
+
if VERSION_FILE.exists():
|
|
275
|
+
VERSION_FILE.unlink()
|
|
276
|
+
# Mark as explicitly disabled — auto-install will respect this
|
|
277
|
+
VERSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
278
|
+
DISABLED_FILE.write_text("removed by user\n")
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
146
281
|
|
|
147
|
-
return
|
|
282
|
+
return result
|
|
148
283
|
|
|
149
284
|
|
|
150
285
|
def check_status() -> dict:
|
|
151
|
-
"""Check
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
286
|
+
"""Check SLM hook installation status."""
|
|
287
|
+
installed_version = ""
|
|
288
|
+
if VERSION_FILE.exists():
|
|
289
|
+
try:
|
|
290
|
+
installed_version = VERSION_FILE.read_text().strip()
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
156
293
|
|
|
157
|
-
|
|
294
|
+
hook_types_found: list[str] = []
|
|
295
|
+
has_gate = False
|
|
158
296
|
try:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
h.get("command", "") == slm_hook_cmd
|
|
165
|
-
for h in session_hooks if isinstance(h, dict)
|
|
166
|
-
)
|
|
297
|
+
settings = _read_settings()
|
|
298
|
+
for hook_type, entries in settings.get("hooks", {}).items():
|
|
299
|
+
if any(_is_slm_hook_entry(e) for e in entries):
|
|
300
|
+
hook_types_found.append(hook_type)
|
|
301
|
+
has_gate = "PreToolUse" in hook_types_found
|
|
167
302
|
except Exception:
|
|
168
303
|
pass
|
|
169
304
|
|
|
305
|
+
installed = len(hook_types_found) >= 3
|
|
306
|
+
|
|
170
307
|
return {
|
|
171
|
-
"installed":
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
308
|
+
"installed": installed,
|
|
309
|
+
"version": installed_version,
|
|
310
|
+
"latest_version": HOOKS_VERSION,
|
|
311
|
+
"needs_upgrade": bool(installed_version and installed_version != HOOKS_VERSION),
|
|
312
|
+
"hook_types": hook_types_found,
|
|
313
|
+
"gate_enabled": has_gate,
|
|
175
314
|
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def upgrade_hooks() -> dict:
|
|
318
|
+
"""Upgrade existing hooks to current version. Non-interactive."""
|
|
319
|
+
status = check_status()
|
|
320
|
+
|
|
321
|
+
if not status["installed"] and not status["version"]:
|
|
322
|
+
return {"upgraded": False, "reason": "No hooks installed"}
|
|
323
|
+
|
|
324
|
+
include_gate = status["gate_enabled"]
|
|
325
|
+
result = install_hooks(include_gate=include_gate)
|
|
326
|
+
result["upgraded"] = result["success"]
|
|
327
|
+
result["from_version"] = status["version"]
|
|
328
|
+
result["to_version"] = HOOKS_VERSION
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def auto_install_if_needed() -> dict | None:
|
|
333
|
+
"""Auto-install hooks if not present and not explicitly disabled.
|
|
334
|
+
|
|
335
|
+
Called from MCP server startup and npm postinstall.
|
|
336
|
+
Returns install result, or None if skipped.
|
|
337
|
+
|
|
338
|
+
Fast path: version file exists and matches → ~0.1ms, returns None.
|
|
339
|
+
"""
|
|
340
|
+
try:
|
|
341
|
+
# Respect explicit opt-out
|
|
342
|
+
if DISABLED_FILE.exists():
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
# Already installed and current → skip
|
|
346
|
+
if VERSION_FILE.exists():
|
|
347
|
+
installed = VERSION_FILE.read_text().strip()
|
|
348
|
+
if installed == HOOKS_VERSION:
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
# Install with clear message
|
|
352
|
+
result = install_hooks(include_gate=False)
|
|
353
|
+
if result["success"]:
|
|
354
|
+
logger.info(
|
|
355
|
+
"SLM: Hooks installed into Claude Code (slm hooks remove to undo)"
|
|
356
|
+
)
|
|
357
|
+
return result
|
|
358
|
+
except Exception as exc:
|
|
359
|
+
logger.debug("Auto-install check failed: %s", exc)
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def auto_upgrade_check() -> None:
|
|
364
|
+
"""Silent auto-upgrade on version mismatch. ~0.1ms when current."""
|
|
365
|
+
try:
|
|
366
|
+
if not VERSION_FILE.exists():
|
|
367
|
+
legacy_script = VERSION_DIR / "slm-session-start.sh"
|
|
368
|
+
if legacy_script.exists():
|
|
369
|
+
_migrate_legacy_hooks()
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
installed = VERSION_FILE.read_text().strip()
|
|
373
|
+
if installed == HOOKS_VERSION:
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
result = upgrade_hooks()
|
|
377
|
+
if result.get("upgraded"):
|
|
378
|
+
logger.info("SLM hooks upgraded %s -> %s", installed, HOOKS_VERSION)
|
|
379
|
+
except Exception as exc:
|
|
380
|
+
logger.debug("Hook auto-upgrade failed: %s", exc)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _migrate_legacy_hooks() -> None:
|
|
384
|
+
"""Migrate from bash-script hooks (pre-3.3.6) to hybrid hooks."""
|
|
385
|
+
try:
|
|
386
|
+
settings = _read_settings()
|
|
387
|
+
has_legacy = False
|
|
388
|
+
for entries in settings.get("hooks", {}).values():
|
|
389
|
+
for e in entries:
|
|
390
|
+
for h in e.get("hooks", []):
|
|
391
|
+
if ".superlocalmemory/hooks/" in h.get("command", ""):
|
|
392
|
+
has_legacy = True
|
|
393
|
+
break
|
|
394
|
+
|
|
395
|
+
if has_legacy:
|
|
396
|
+
settings = _remove_slm_hooks(settings)
|
|
397
|
+
hook_defs = _hook_definitions(include_gate=False)
|
|
398
|
+
settings = _merge_hooks(settings, hook_defs)
|
|
399
|
+
_write_settings(settings)
|
|
400
|
+
VERSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
401
|
+
VERSION_FILE.write_text(HOOKS_VERSION)
|
|
402
|
+
logger.info("Migrated legacy bash hooks to hybrid hooks (v%s)", HOOKS_VERSION)
|
|
403
|
+
except Exception as exc:
|
|
404
|
+
logger.debug("Legacy hook migration failed: %s", exc)
|