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 CHANGED
@@ -3,7 +3,8 @@
3
3
  </p>
4
4
 
5
5
  <h1 align="center">SuperLocalMemory V3.3</h1>
6
- <p align="center"><strong>The first local-only AI memory to break 74% retrieval on LoCoMo.<br/>No cloud. No APIs. No data leaves your machine.</strong></p>
6
+ <p align="center"><strong>Every other AI forgets. Yours won't.</strong><br/><em>Infinite memory for Claude Code, Cursor, Windsurf & 17+ AI tools.</em></p>
7
+ <p align="center"><code>v3.3.6</code> — Install once. Every session remembers the last. Automatically.</p>
7
8
 
8
9
  <p align="center">
9
10
  <code>+16pp vs Mem0 (zero cloud)</code> &nbsp;·&nbsp; <code>85% Open-Domain (best of any system)</code> &nbsp;·&nbsp; <code>EU AI Act Ready</code>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.3.5",
3
+ "version": "3.3.7",
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.3.5"
3
+ version = "3.3.7"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -213,17 +213,48 @@ if (fs.existsSync(V2_HOME) && fs.existsSync(path.join(V2_HOME, 'memory.db'))) {
213
213
  console.log('');
214
214
  }
215
215
 
216
+ // --- Step 5: Auto-install Claude Code hooks ---
217
+ // "Install once, forget forever" — hooks enable automatic memory lifecycle
218
+ const hooksDisabledFile = path.join(SLM_HOME, 'hooks', '.hooks-disabled');
219
+ if (fs.existsSync(hooksDisabledFile)) {
220
+ console.log('⊘ Claude Code hooks: skipped (user opted out via slm hooks remove)');
221
+ } else {
222
+ console.log('\nInstalling Claude Code hooks (auto-memory lifecycle)...');
223
+ const hookResult = spawnSync(pythonParts[0], [
224
+ ...pythonParts.slice(1), '-m', 'superlocalmemory.cli.main', 'hooks', 'install',
225
+ ], {
226
+ stdio: 'pipe', timeout: 15000,
227
+ env: {
228
+ ...process.env,
229
+ PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:' + (process.env.PATH || ''),
230
+ PYTHONPATH: path.join(__dirname, '..', 'src') + ':' + (process.env.PYTHONPATH || ''),
231
+ },
232
+ });
233
+
234
+ if (hookResult.status === 0) {
235
+ console.log('✓ Claude Code hooks installed (auto-recall, auto-observe, auto-save)');
236
+ console.log(' SLM: Hooks installed into Claude Code (slm hooks remove to undo)');
237
+ } else {
238
+ console.log('⚠ Claude Code hooks not installed (run: slm hooks install)');
239
+ // Non-fatal — don't block npm install
240
+ }
241
+ }
242
+
216
243
  // --- Done ---
217
244
  console.log('════════════════════════════════════════════════════════════');
218
245
  console.log(' ✓ SuperLocalMemory V3 installed successfully!');
219
246
  console.log('');
220
247
  console.log(' Quick start:');
221
- console.log(' slm setup # First-time configuration');
248
+ console.log(' Just open Claude Code — memory works automatically!');
249
+ console.log('');
250
+ console.log(' Other commands:');
222
251
  console.log(' slm doctor # Pre-flight check (verify everything works)');
223
252
  console.log(' slm warmup # Pre-download embedding model (~500MB)');
224
253
  console.log(' slm remember "..." # Store a memory');
225
254
  console.log(' slm recall "..." # Search memories');
226
255
  console.log(' slm dashboard # Open 17-tab web dashboard');
256
+ console.log(' slm hooks status # Check hook installation');
257
+ console.log(' slm hooks remove # Opt out of auto-memory hooks');
227
258
  console.log('');
228
259
  console.log(' Prerequisites satisfied:');
229
260
  console.log(' ✓ Python 3.11+');
@@ -18,7 +18,16 @@ from argparse import Namespace
18
18
 
19
19
  def dispatch(args: Namespace) -> None:
20
20
  """Route CLI command to the appropriate handler."""
21
+ # Auto-install/upgrade hooks on version change (single file read, ~0.1ms)
22
+ if args.command not in ("hooks", "init", "mcp"):
23
+ try:
24
+ from superlocalmemory.hooks.claude_code_hooks import auto_install_if_needed
25
+ auto_install_if_needed()
26
+ except Exception:
27
+ pass
28
+
21
29
  handlers = {
30
+ "init": cmd_init,
22
31
  "setup": cmd_setup,
23
32
  "mode": cmd_mode,
24
33
  "provider": cmd_provider,
@@ -923,6 +932,14 @@ def cmd_trace(args: Namespace) -> None:
923
932
 
924
933
  def cmd_mcp(_args: Namespace) -> None:
925
934
  """Start the V3 MCP server (stdio transport for IDE integration)."""
935
+ # Auto-install hooks on MCP startup (fast path: ~0.1ms if already current)
936
+ # CRITICAL: No stdout — MCP uses stdio transport, any print corrupts protocol
937
+ try:
938
+ from superlocalmemory.hooks.claude_code_hooks import auto_install_if_needed
939
+ auto_install_if_needed()
940
+ except Exception:
941
+ pass
942
+
926
943
  from superlocalmemory.mcp.server import server
927
944
 
928
945
  server.run(transport="stdio")
@@ -1142,6 +1159,99 @@ def cmd_profile(args: Namespace) -> None:
1142
1159
  # -- Active Memory commands (V3.1) ------------------------------------------
1143
1160
 
1144
1161
 
1162
+ def cmd_init(args: Namespace) -> None:
1163
+ """One-command setup: mode + hooks + IDE connect + warmup."""
1164
+ from pathlib import Path
1165
+ from superlocalmemory.core.config import SLMConfig
1166
+
1167
+ force = getattr(args, "force", False)
1168
+
1169
+ config_exists = (Path.home() / ".superlocalmemory" / "config.json").exists()
1170
+
1171
+ print()
1172
+ print("SuperLocalMemory — One-Time Setup")
1173
+ print("=" * 40)
1174
+
1175
+ # Step 1: Mode selection (interactive)
1176
+ if force or not config_exists:
1177
+ print()
1178
+ from superlocalmemory.cli.setup_wizard import run_wizard
1179
+ run_wizard()
1180
+ else:
1181
+ config = SLMConfig.load()
1182
+ print(f"\n Already configured: Mode {config.mode.value.upper()}")
1183
+ print(f" Profile: {config.active_profile}")
1184
+
1185
+ # Step 2: Install hooks (gate always OFF by default)
1186
+ print()
1187
+ print("Installing Claude Code hooks...")
1188
+ from superlocalmemory.hooks.claude_code_hooks import install_hooks, check_status
1189
+
1190
+ status = check_status()
1191
+
1192
+ if status["installed"] and not force:
1193
+ if status["needs_upgrade"]:
1194
+ from superlocalmemory.hooks.claude_code_hooks import upgrade_hooks
1195
+ result = upgrade_hooks()
1196
+ if result.get("upgraded"):
1197
+ print(f" Hooks upgraded: {result['from_version']} -> {result['to_version']}")
1198
+ else:
1199
+ print(f" Upgrade issue: {result.get('reason', result.get('errors', ''))}")
1200
+ else:
1201
+ print(f" Hooks already installed (v{status['version']})")
1202
+ else:
1203
+ result = install_hooks(include_gate=False)
1204
+ if result["success"]:
1205
+ print(f" Hooks installed: {', '.join(result['hooks_added'])}")
1206
+ print(" SLM: Hooks installed into Claude Code (slm hooks remove to undo)")
1207
+ else:
1208
+ print(f" Hook install failed: {result['errors']}")
1209
+
1210
+ # Step 3: IDE connection
1211
+ print()
1212
+ print("Detecting IDEs...")
1213
+ try:
1214
+ from superlocalmemory.hooks.ide_connector import IDEConnector
1215
+ connector = IDEConnector()
1216
+ results = connector.connect_all()
1217
+ for ide_id, ide_status in results.items():
1218
+ print(f" {ide_id}: {ide_status}")
1219
+ except Exception as exc:
1220
+ print(f" IDE detection skipped: {exc}")
1221
+
1222
+ # Step 4: Warmup (embedding model)
1223
+ print()
1224
+ print("Checking embedding model...")
1225
+ try:
1226
+ from superlocalmemory.core.config import SLMConfig as _Cfg
1227
+ cfg = _Cfg.load()
1228
+ model_name = cfg.embedding.model_name
1229
+ print(f" Model: {model_name}")
1230
+ # Quick check: try creating embedding service (auto-downloads if needed)
1231
+ from superlocalmemory.core.embeddings import EmbeddingService
1232
+ svc = EmbeddingService(cfg.embedding)
1233
+ test_result = svc.embed_text("test")
1234
+ if test_result is not None and len(test_result) > 0:
1235
+ print(" Status: ready")
1236
+ else:
1237
+ print(" Status: model not available (run: slm warmup)")
1238
+ except Exception as exc:
1239
+ print(f" Warmup skipped: {exc}")
1240
+ print(" Run 'slm warmup' later to download the embedding model.")
1241
+
1242
+ # Done
1243
+ print()
1244
+ print("=" * 40)
1245
+ print("SLM is active. Your AI now remembers you.")
1246
+ print()
1247
+ print("What happens next:")
1248
+ print(" - Open Claude Code in any project")
1249
+ print(" - SLM auto-injects your memory context")
1250
+ print(" - Decisions, bugs, preferences are captured automatically")
1251
+ print(" - Session summaries saved when you close")
1252
+ print()
1253
+
1254
+
1145
1255
  def cmd_hooks(args: Namespace) -> None:
1146
1256
  """Manage Claude Code hooks for invisible memory injection."""
1147
1257
  from superlocalmemory.hooks.claude_code_hooks import (
@@ -1149,28 +1259,38 @@ def cmd_hooks(args: Namespace) -> None:
1149
1259
  )
1150
1260
 
1151
1261
  action = getattr(args, "action", "status")
1262
+ # Gate is OFF by default. --gate opts in (for brave users).
1263
+ include_gate = getattr(args, "gate", False)
1264
+
1152
1265
  if action == "install":
1153
- result = install_hooks()
1154
- if result["scripts"] and result["settings"]:
1266
+ result = install_hooks(include_gate=include_gate)
1267
+ if result["success"]:
1155
1268
  print("SLM hooks installed in Claude Code.")
1156
- print("Memory context will auto-inject on every new session.")
1269
+ print(f" Hook types: {', '.join(result['hooks_added'])}")
1270
+ if include_gate:
1271
+ print(" Gate: ON (enforces session_init — experimental)")
1272
+ print(" SLM: Hooks installed into Claude Code (slm hooks remove to undo)")
1157
1273
  else:
1158
- print(f"Installation incomplete: {result['errors']}")
1274
+ print(f"Installation failed: {result['errors']}")
1159
1275
  elif action == "remove":
1160
1276
  result = remove_hooks()
1161
- if result["scripts"] and result["settings"]:
1277
+ if result["success"]:
1162
1278
  print("SLM hooks removed from Claude Code.")
1163
1279
  else:
1164
- print(f"Removal incomplete: {result['errors']}")
1280
+ print(f"Removal failed: {result['errors']}")
1165
1281
  else:
1166
1282
  result = check_status()
1167
1283
  if result["installed"]:
1168
- print("SLM hooks: INSTALLED")
1169
- print(f" Scripts: {result['hooks_dir']}")
1170
- print(" Claude Code settings: configured")
1284
+ print(f"SLM hooks: INSTALLED (v{result['version']})")
1285
+ print(f" Hook types: {', '.join(result['hook_types'])}")
1286
+ print(f" Gate: {'ON' if result['gate_enabled'] else 'OFF'}")
1287
+ if result["needs_upgrade"]:
1288
+ print(f" Update available: {result['version']} -> {result['latest_version']}")
1289
+ print(" Run: slm hooks install")
1171
1290
  else:
1172
1291
  print("SLM hooks: NOT INSTALLED")
1173
1292
  print(" Run: slm hooks install")
1293
+ print(" Or: slm init (full setup)")
1174
1294
 
1175
1295
 
1176
1296
  def cmd_session_context(args: Namespace) -> None:
@@ -70,6 +70,12 @@ documentation:
70
70
 
71
71
  def main() -> None:
72
72
  """Parse CLI arguments and dispatch to command handlers."""
73
+ # Fast path: hook invocations bypass argparse entirely (stdlib only, ~30ms)
74
+ if len(sys.argv) >= 3 and sys.argv[1] == "hook":
75
+ from superlocalmemory.hooks.hook_handlers import handle_hook
76
+ handle_hook(sys.argv[2])
77
+ return
78
+
73
79
  from superlocalmemory.cli.json_output import _get_version
74
80
  _ver = _get_version()
75
81
 
@@ -85,6 +91,15 @@ def main() -> None:
85
91
  sub = parser.add_subparsers(dest="command", title="commands")
86
92
 
87
93
  # -- Setup & Config ------------------------------------------------
94
+ init_p = sub.add_parser("init", help="One-command setup: mode + hooks + IDE + warmup")
95
+ init_p.add_argument(
96
+ "--force", action="store_true", help="Re-run full setup even if already configured",
97
+ )
98
+ init_p.add_argument(
99
+ "--gate", action="store_true",
100
+ help="Enable PreToolUse gate (experimental — blocks tools until session_init)",
101
+ )
102
+
88
103
  sub.add_parser("setup", help="Interactive first-time setup wizard")
89
104
 
90
105
  mode_p = sub.add_parser("mode", help="Get or set operating mode (a/b/c)")
@@ -182,6 +197,10 @@ def main() -> None:
182
197
  "action", nargs="?", default="status",
183
198
  choices=["install", "remove", "status"], help="Action (default: status)",
184
199
  )
200
+ hooks_p.add_argument(
201
+ "--gate", action="store_true",
202
+ help="Enable PreToolUse gate (experimental — blocks tools until session_init)",
203
+ )
185
204
 
186
205
  ctx_p = sub.add_parser("session-context", help="Print session context (for hooks)")
187
206
  ctx_p.add_argument("query", nargs="?", default="", help="Optional context query")
@@ -23,9 +23,10 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
23
23
  from __future__ import annotations
24
24
 
25
25
  import json
26
+ import os
26
27
  import signal
27
28
  import sys
28
- import os
29
+ import threading
29
30
 
30
31
  # Force CPU BEFORE any torch import
31
32
  os.environ["CUDA_VISIBLE_DEVICES"] = ""
@@ -41,8 +42,33 @@ if sys.platform != "win32":
41
42
  signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
42
43
 
43
44
 
45
+ def _start_parent_watchdog() -> None:
46
+ """Monitor parent process — self-terminate if parent dies.
47
+
48
+ Prevents orphaned workers that consume 500-800 MB each when the parent
49
+ process crashes, is killed, or exits without cleanup.
50
+
51
+ V3.3.7: Added after incident where orphaned workers consumed 33 GB.
52
+ """
53
+ parent_pid = os.getppid()
54
+
55
+ def _watch() -> None:
56
+ import time
57
+ while True:
58
+ time.sleep(5)
59
+ try:
60
+ os.kill(parent_pid, 0)
61
+ except OSError:
62
+ os._exit(0)
63
+
64
+ t = threading.Thread(target=_watch, daemon=True, name="parent-watchdog")
65
+ t.start()
66
+
67
+
44
68
  def _worker_main() -> None:
45
69
  """Main loop: read JSON requests from stdin, write responses to stdout."""
70
+ _start_parent_watchdog() # V3.3.7: self-terminate if parent dies
71
+
46
72
  import numpy as np
47
73
 
48
74
  model = None
@@ -15,6 +15,7 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import atexit
18
19
  import json
19
20
  import logging
20
21
  import os
@@ -22,11 +23,15 @@ import subprocess
22
23
  import sys
23
24
  import threading
24
25
  import time
26
+ import weakref
25
27
  from pathlib import Path
26
28
  from typing import TYPE_CHECKING
27
29
 
28
30
  import numpy as np
29
31
 
32
+ # Track all live embedding services for atexit cleanup
33
+ _live_embedding_services: set[weakref.ref] = set()
34
+
30
35
  if TYPE_CHECKING:
31
36
  from numpy.typing import NDArray
32
37
 
@@ -69,6 +74,17 @@ class EmbeddingService:
69
74
  self._worker_ready = False
70
75
  self._request_count: int = 0
71
76
 
77
+ # Register for atexit cleanup (prevent orphaned workers)
78
+ ref = weakref.ref(self, _live_embedding_services.discard)
79
+ _live_embedding_services.add(ref)
80
+
81
+ def __del__(self) -> None:
82
+ """Kill worker subprocess when service is garbage-collected."""
83
+ try:
84
+ self._kill_worker()
85
+ except Exception:
86
+ pass
87
+
72
88
  @property
73
89
  def is_available(self) -> bool:
74
90
  """Check if embedding service can produce embeddings."""
@@ -338,3 +354,26 @@ class EmbeddingService:
338
354
  raise DimensionMismatchError(
339
355
  f"Embedding dimension {actual} != expected {self._config.dimension}"
340
356
  )
357
+
358
+
359
+ # ---------------------------------------------------------------------------
360
+ # Module-level atexit: kill ALL embedding workers on process exit
361
+ # ---------------------------------------------------------------------------
362
+
363
+ def _cleanup_all_embedding_services() -> None:
364
+ """Kill all embedding worker subprocesses on interpreter exit.
365
+
366
+ Prevents orphaned 500-800 MB sentence-transformer workers surviving
367
+ after parent exits (especially during test runs with parallel agents).
368
+ """
369
+ for ref in list(_live_embedding_services):
370
+ svc = ref()
371
+ if svc is not None:
372
+ try:
373
+ svc._kill_worker()
374
+ except Exception:
375
+ pass
376
+ _live_embedding_services.clear()
377
+
378
+
379
+ atexit.register(_cleanup_all_embedding_services)
@@ -20,6 +20,7 @@ import json
20
20
  import os
21
21
  import signal
22
22
  import sys
23
+ import threading
23
24
 
24
25
  # Force CPU BEFORE any torch import
25
26
  os.environ["CUDA_VISIBLE_DEVICES"] = ""
@@ -34,6 +35,29 @@ os.environ["TORCH_DEVICE"] = "cpu"
34
35
  if sys.platform != "win32":
35
36
  signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
36
37
 
38
+
39
+ def _start_parent_watchdog() -> None:
40
+ """Monitor parent process — self-terminate if parent dies.
41
+
42
+ Prevents orphaned workers that consume 500+ MB each when the parent
43
+ process crashes, is killed, or exits without cleanup.
44
+
45
+ V3.3.7: Added after incident where orphaned workers consumed 33 GB.
46
+ """
47
+ parent_pid = os.getppid()
48
+
49
+ def _watch() -> None:
50
+ import time
51
+ while True:
52
+ time.sleep(5)
53
+ try:
54
+ os.kill(parent_pid, 0)
55
+ except OSError:
56
+ os._exit(0)
57
+
58
+ t = threading.Thread(target=_watch, daemon=True, name="parent-watchdog")
59
+ t.start()
60
+
37
61
  _engine = None
38
62
 
39
63
 
@@ -209,6 +233,8 @@ def _handle_status() -> dict:
209
233
 
210
234
  def _worker_main() -> None:
211
235
  """Main loop: read JSON requests from stdin, write responses to stdout."""
236
+ _start_parent_watchdog() # V3.3.7: self-terminate if parent dies
237
+
212
238
  for line in sys.stdin:
213
239
  line = line.strip()
214
240
  if not line: