superlocalmemory 3.8.1 → 3.8.3

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +2 -2
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +3 -5
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +2 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +3 -5
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +1 -1
  32. package/scripts/postinstall.js +7 -1
  33. package/src/superlocalmemory/__init__.py +1 -1
  34. package/src/superlocalmemory/cli/commands.py +360 -2
  35. package/src/superlocalmemory/cli/main.py +62 -3
  36. package/src/superlocalmemory/cli/setup_wizard.py +142 -16
  37. package/src/superlocalmemory/core/component_healer.py +144 -0
  38. package/src/superlocalmemory/core/component_registry.py +487 -0
  39. package/src/superlocalmemory/core/config.py +21 -0
  40. package/src/superlocalmemory/core/embeddings.py +14 -1
  41. package/src/superlocalmemory/core/engine.py +9 -5
  42. package/src/superlocalmemory/core/ingestion_command.py +36 -16
  43. package/src/superlocalmemory/core/maintenance.py +43 -0
  44. package/src/superlocalmemory/core/maintenance_scheduler.py +28 -0
  45. package/src/superlocalmemory/core/recall_pipeline.py +39 -3
  46. package/src/superlocalmemory/core/store_pipeline.py +42 -0
  47. package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
  48. package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
  49. package/src/superlocalmemory/mcp/tools_active.py +1 -1
  50. package/src/superlocalmemory/mcp/tools_core.py +17 -2
  51. package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
  52. package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
  53. package/src/superlocalmemory/server/routes/behavioral.py +6 -2
  54. package/src/superlocalmemory/server/routes/learning.py +13 -3
  55. package/src/superlocalmemory/server/routes/memories.py +80 -26
  56. package/src/superlocalmemory/server/routes/v3_api.py +120 -0
  57. package/src/superlocalmemory/server/unified_daemon.py +349 -10
  58. package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
  59. package/src/superlocalmemory/ui/index.html +3 -2
  60. package/src/superlocalmemory/ui/js/core.js +6 -1
  61. package/src/superlocalmemory/ui/js/od-components.js +147 -0
  62. package/src/superlocalmemory/ui/js/od-entities.js +43 -0
  63. package/src/superlocalmemory/ui/js/od-graph.js +35 -0
  64. package/src/superlocalmemory/ui/js/od-health.js +18 -0
  65. package/src/superlocalmemory/ui/js/od-memories.js +37 -0
  66. package/src/superlocalmemory/ui/js/od-operations.js +36 -0
  67. package/src/superlocalmemory/ui/js/od-settings.js +72 -3
@@ -100,6 +100,25 @@ def _get_ram_gb() -> float:
100
100
  return 0.0
101
101
 
102
102
 
103
+ def _ollama_available() -> bool:
104
+ """True if a local Ollama server is reachable (or its binary is installed).
105
+
106
+ v3.8.2: used to RECOMMEND Mode B at setup with a single keypress. Bounded
107
+ and fail-safe — a slow/absent Ollama never blocks or slows the wizard.
108
+ """
109
+ try:
110
+ import httpx
111
+
112
+ if httpx.get("http://localhost:11434/api/tags", timeout=1.5).status_code == 200:
113
+ return True
114
+ except Exception:
115
+ pass
116
+ try:
117
+ return shutil.which("ollama") is not None
118
+ except Exception:
119
+ return False
120
+
121
+
103
122
  # ---------------------------------------------------------------------------
104
123
  # Model download
105
124
  # ---------------------------------------------------------------------------
@@ -388,14 +407,23 @@ def run_wizard(auto: bool = False) -> None:
388
407
  return
389
408
 
390
409
  # -- Step 2: Mode selection --
410
+ # v3.8.2: pre-detect Ollama so we can recommend Mode B with one keypress.
411
+ # No detection → zero-config Mode A. (Non-interactive always stays A: Mode B
412
+ # needs a pulled model, so we never assume it without the user confirming.)
413
+ ollama_present = _ollama_available()
414
+ default_mode = "b" if ollama_present else "a"
391
415
  print()
392
416
  print("─── Step 2/10: Choose Operating Mode ───")
393
417
  print()
394
- print(" [A] Local Guardian (recommended)")
418
+ if ollama_present:
419
+ print(" ✓ Ollama detected on this machine — Mode B recommended.")
420
+ print(" Press Enter to accept, or pick another mode.")
421
+ print()
422
+ print(" [A] Local Guardian" + ("" if ollama_present else " (recommended)"))
395
423
  print(" No model-provider call in the core memory path.")
396
424
  print(" Review optional integrations and network policy for your deployment.")
397
425
  print()
398
- print(" [B] Smart Local")
426
+ print(" [B] Smart Local" + (" (recommended — Ollama detected)" if ollama_present else ""))
399
427
  print(" Local LLM via Ollama for enrichment.")
400
428
  print(" Data stays on your machine.")
401
429
  print()
@@ -405,10 +433,13 @@ def run_wizard(auto: bool = False) -> None:
405
433
  print()
406
434
 
407
435
  if interactive:
408
- choice = _prompt(" Select mode [A/B/C] (default: A): ", "a").lower()
436
+ choice = _prompt(
437
+ f" Select mode [A/B/C] (default: {default_mode.upper()}): ",
438
+ default_mode,
439
+ ).lower()
409
440
  else:
410
441
  choice = "a"
411
- print(" Auto-selecting Mode A (non-interactive)")
442
+ print(" Auto-selecting Mode A (non-interactive, zero-config)")
412
443
 
413
444
  if choice not in ("a", "b", "c"):
414
445
  print(f" Invalid choice '{choice}', using Mode A.")
@@ -496,6 +527,15 @@ def run_wizard(auto: bool = False) -> None:
496
527
  else:
497
528
  print(f"\n ✓ CodeGraph disabled (enable later in {cg_config_path})")
498
529
 
530
+ # v3.8.2: up-front download summary so a large pull is never a surprise on
531
+ # a metered/slow link. Sizes approximate, one-time, cached locally.
532
+ if st_ok and not _embedding_is_remote(config):
533
+ print()
534
+ print(" Models to download (one-time, cached locally):")
535
+ print(" • Embedding model ~500 MB — required for semantic recall")
536
+ print(" • Reranker model ~130 MB — result quality")
537
+ print(" • Compression model ~560 MB — optional (you'll be asked)")
538
+
499
539
  # -- Step 4: Download models --
500
540
  print()
501
541
  # H-06 (CVE-2025-14926): a malicious HuggingFace checkpoint can execute code
@@ -532,8 +572,25 @@ def run_wizard(auto: bool = False) -> None:
532
572
  _download_reranker(_RERANKER_MODEL)
533
573
 
534
574
  print()
535
- print("─── Step 4c/10: Download Compression Model (LLMLingua-2) ───")
536
- _download_compressor(_COMPRESSOR_MODEL)
575
+ print("─── Step 4c/10: Compression Model (LLMLingua-2, ~560MB) ───")
576
+ print(" Only used for aggressive prompt compression. It also downloads")
577
+ print(" automatically the first time you enable that feature — so skipping")
578
+ print(" here is safe.")
579
+ # v3.8.2: consent-gate the 560MB compressor (was unconditional). Skip by
580
+ # default; it lazy-downloads on first real use of compression.
581
+ if not st_ok:
582
+ print(" ⚠ Skipped (sentence-transformers not installed)")
583
+ elif interactive:
584
+ comp_choice = _prompt(
585
+ " Download the ~560MB compression model now? [y/N] (default: N): ",
586
+ "n",
587
+ ).lower()
588
+ if comp_choice in ("y", "yes"):
589
+ _download_compressor(_COMPRESSOR_MODEL)
590
+ else:
591
+ print(" ✓ Skipped — downloads automatically when you enable compression.")
592
+ else:
593
+ print(" ✓ Skipped (non-interactive) — downloads on first use.")
537
594
 
538
595
  # -- Step 5: Daemon Configuration (v3.4.3) --
539
596
  print()
@@ -655,17 +712,21 @@ def run_wizard(auto: bool = False) -> None:
655
712
  print(" It detects degradation, generates improvements, and verifies them blindly.")
656
713
  print(" Requires an LLM backend (Claude CLI, Ollama, or API key).")
657
714
  print()
715
+ print(" OFF by default — when enabled it makes background LLM calls.")
658
716
  print(" [Y] Enable Skill Evolution")
659
- print(" [N] Disable (can enable later: slm config set evolution.enabled true)")
717
+ print(" [N] Keep disabled (enable later: slm config set evolution.enabled true)")
660
718
  print()
661
719
 
720
+ # v3.8.2: default OFF (was Y) — consistent with the npm profiles and no
721
+ # surprise outbound LLM calls for a fresh user. Opt-in, not opt-out.
662
722
  if interactive:
663
- evo_choice = _prompt(" Enable Skill Evolution? [Y/n] (default: Y): ", "y").lower()
723
+ evo_choice = _prompt(" Enable Skill Evolution? [y/N] (default: N): ", "n").lower()
664
724
  else:
665
- evo_choice = "y"
666
- print(" Auto-enabling Skill Evolution (non-interactive)")
725
+ evo_choice = "n"
726
+ print(" Skill Evolution stays OFF (non-interactive) — enable later with:")
727
+ print(" slm config set evolution.enabled true")
667
728
 
668
- evolution_enabled = evo_choice in ("", "y", "yes")
729
+ evolution_enabled = evo_choice in ("y", "yes")
669
730
 
670
731
  # Write evolution config to config.json directly
671
732
  # (SLMConfig.save() doesn't serialize evolution)
@@ -753,6 +814,21 @@ def run_wizard(auto: bool = False) -> None:
753
814
  else:
754
815
  print(" Adapters: none (enable via: slm adapters enable gmail)")
755
816
  print()
817
+ # v3.8.2: surface component self-heal status so the user leaves setup
818
+ # KNOWING everything they need is present — or exactly what to run if not.
819
+ try:
820
+ from superlocalmemory.core import component_registry as _cr
821
+
822
+ _sum = _cr.snapshot(config)["summary"]
823
+ if _sum.get("healthy"):
824
+ print(" ✓ All components ready — models and dependencies present.")
825
+ else:
826
+ print(f" ⚠ {_sum.get('missing', 0)} component(s) need attention.")
827
+ print(" SLM auto-repairs fixable ones on daemon start. To check/fix now:")
828
+ print(" slm doctor (auto-fix: slm doctor --fix)")
829
+ except Exception:
830
+ pass
831
+ print()
756
832
  print(" Quick start:")
757
833
  print(' slm remember "your first memory"')
758
834
  print(' slm recall "search query"')
@@ -760,8 +836,8 @@ def run_wizard(auto: bool = False) -> None:
760
836
  print(" slm adapters enable gmail → start Gmail ingestion")
761
837
  print()
762
838
  print(" Need help?")
763
- print(" slm doctor diagnose issues")
764
- print(" slm --helpall commands")
839
+ print(" slm help every command, grouped (try: slm help self-heal)")
840
+ print(" slm doctordiagnose issues (auto-fix: slm doctor --fix)")
765
841
  print(" slm serve install — install auto-start service")
766
842
  print(" https://github.com/qualixar/superlocalmemory")
767
843
  print()
@@ -816,20 +892,70 @@ def check_first_use(command: str) -> None:
816
892
 
817
893
 
818
894
  def _configure_external_integrations(*, interactive: bool) -> bool:
819
- """Request consent before editing Claude Code configuration."""
895
+ """Request consent before editing Claude Code configuration.
896
+
897
+ v3.8.2: after the Claude Code step, a pip user can also wire every OTHER
898
+ detected IDE in one step (previously only Claude Code was offered here;
899
+ other IDEs required a manual `slm connect <ide>` each).
900
+ """
820
901
  print()
821
902
  print(" Optional Claude Code integration can install the SLM plugin and hooks.")
822
903
  if not interactive:
823
904
  print(" Skipped in non-interactive setup. Run: slm connect claude-code")
824
905
  return False
906
+ changed = False
825
907
  choice = _prompt(
826
908
  " Install Claude Code plugin and hooks now? [y/N] (default: N): ",
827
909
  "n",
828
910
  ).lower()
829
- if choice not in ("y", "yes"):
911
+ if choice in ("y", "yes"):
912
+ changed = _install_external_integrations()
913
+ else:
830
914
  print(" Skipped. Run `slm connect claude-code` when ready.")
915
+ # v3.8.2: offer to connect all other detected IDEs (Cursor, VS Code, …).
916
+ changed = _configure_other_ides(interactive=interactive) or changed
917
+ return changed
918
+
919
+
920
+ def _configure_other_ides(*, interactive: bool) -> bool:
921
+ """Consent-gated: connect all OTHER detected IDEs (Cursor, VS Code, …).
922
+
923
+ Uses the same IDEConnector the dashboard uses. Best-effort — a failure to
924
+ detect or connect any single IDE never blocks setup.
925
+ """
926
+ if not interactive:
927
+ return False
928
+ try:
929
+ from superlocalmemory.hooks.ide_connector import IDEConnector
930
+
931
+ connector = IDEConnector()
932
+ installed = [s for s in connector.get_status() if s.get("installed")]
933
+ except Exception:
831
934
  return False
832
- return _install_external_integrations()
935
+ others = [s for s in installed if s.get("id") not in ("claude-code", "claude")]
936
+ if not others:
937
+ return False
938
+ names = ", ".join(s.get("name", s.get("id")) for s in others)
939
+ print()
940
+ print(f" Detected other IDEs: {names}")
941
+ print(" SLM can wire them all so memory works across every editor.")
942
+ choice = _prompt(" Connect them now? [Y/n] (default: Y): ", "y").lower()
943
+ if choice not in ("", "y", "yes"):
944
+ print(" Skipped. Connect any later with: slm connect <ide>")
945
+ return False
946
+ changed = False
947
+ for s in others:
948
+ ide_id = s.get("id")
949
+ label = s.get("name", ide_id)
950
+ try:
951
+ if connector.connect(ide_id):
952
+ print(f" ✓ {label}")
953
+ changed = True
954
+ else:
955
+ print(f" ⚠ {label} — connect later: slm connect {ide_id}")
956
+ except Exception:
957
+ print(f" ⚠ {label} — connect later: slm connect {ide_id}")
958
+ return changed
833
959
 
834
960
 
835
961
  def _install_external_integrations() -> bool:
@@ -0,0 +1,144 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Component healer — the repair actions behind the registry (v3.8.2).
6
+
7
+ Detection lives in :mod:`core.component_registry` (read-only probes). This
8
+ module performs the *actions* for components the registry marked
9
+ ``auto_fixable``: re-download a HuggingFace model, or pip-install a small
10
+ pure-python dependency. Both the background self-heal thread
11
+ (``server.unified_daemon._self_heal`` Step 0/0.5) and the foreground
12
+ ``slm doctor --fix`` command call :func:`heal_missing`, so the repair logic
13
+ exists in exactly one place.
14
+
15
+ Safety (Varun's mandate, unchanged):
16
+ * NEVER ``sudo``; NEVER auto ``ollama pull`` (surprise network/disk);
17
+ NEVER auto-install multi-GB deps (torch) — those are manual fix commands.
18
+ * pip only into a user-writable interpreter (no PEP-668 marker).
19
+ * Bounded retries; every action is fail-open — a repair failure never
20
+ raises into the caller and never wedges recall.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import subprocess
26
+ import sys
27
+ import time
28
+ from typing import Any, Callable
29
+
30
+ from superlocalmemory.core import component_registry as cr
31
+
32
+ # Progress sink: (component_key, human_message) -> None. Defaults to no-op.
33
+ ProgressFn = Callable[[str, str], None]
34
+
35
+
36
+ def _noop(_key: str, _msg: str) -> None:
37
+ pass
38
+
39
+
40
+ def _pip_install(package: str, timeout: int = 300) -> tuple[bool, str]:
41
+ """pip-install one package into the current interpreter. Fail-open."""
42
+ try:
43
+ result = subprocess.run(
44
+ [sys.executable, "-m", "pip", "install", "--no-input", package],
45
+ timeout=timeout, capture_output=True, text=True,
46
+ )
47
+ if result.returncode == 0:
48
+ return True, f"installed {package}"
49
+ tail = (result.stderr or result.stdout or "").strip().splitlines()
50
+ return False, (tail[-1] if tail else f"pip exit {result.returncode}")
51
+ except subprocess.TimeoutExpired:
52
+ return False, f"pip install {package} timed out ({timeout}s)"
53
+ except Exception as exc: # never propagate — heal is fail-open
54
+ return False, f"{type(exc).__name__}: {exc}"
55
+
56
+
57
+ def _heal_embedder() -> tuple[bool, str]:
58
+ from superlocalmemory.cli.setup_wizard import _EMBED_MODEL, _download_model
59
+
60
+ ok = _download_model(_EMBED_MODEL, "Embedding model")
61
+ return ok, "embedding model ready" if ok else "download failed"
62
+
63
+
64
+ def _heal_reranker() -> tuple[bool, str]:
65
+ from superlocalmemory.cli.setup_wizard import _RERANKER_MODEL, _download_reranker
66
+
67
+ ok = _download_reranker(_RERANKER_MODEL)
68
+ return ok, "reranker ready" if ok else "download failed"
69
+
70
+
71
+ def _heal_sqlite_vec() -> tuple[bool, str]:
72
+ if not cr.pip_is_user_writable():
73
+ return False, "interpreter externally managed (PEP 668) — install manually"
74
+ return _pip_install("sqlite-vec")
75
+
76
+
77
+ # key -> action. Only keys the registry can mark auto_fixable appear here.
78
+ _ACTIONS: dict[str, Callable[[], tuple[bool, str]]] = {
79
+ "embedder_model": _heal_embedder,
80
+ "reranker_model": _heal_reranker,
81
+ "sqlite_vec": _heal_sqlite_vec,
82
+ }
83
+
84
+
85
+ def heal_missing(
86
+ config: Any = None,
87
+ keys: list[str] | None = None,
88
+ on_progress: ProgressFn | None = None,
89
+ max_retries: int = 2,
90
+ ) -> dict[str, Any]:
91
+ """Repair every auto-fixable missing component (optionally filtered to ``keys``).
92
+
93
+ Returns ``{attempted, healed, failed, results}`` where ``results`` is a
94
+ list of ``{key, success, detail}``. Marks each component ``retrying`` in
95
+ the registry's transient overlay while its repair is in flight so the
96
+ dashboard shows live progress; clears the marker on success (a fresh
97
+ probe then confirms ``ok``) or on give-up (probe reports ``missing`` again).
98
+ """
99
+ progress = on_progress or _noop
100
+ targets = cr.auto_fixable_missing(config)
101
+ if keys is not None:
102
+ wanted = set(keys)
103
+ targets = [c for c in targets if c.key in wanted]
104
+
105
+ results: list[dict[str, Any]] = []
106
+ healed = failed = 0
107
+
108
+ for comp in targets:
109
+ action = _ACTIONS.get(comp.key)
110
+ if action is None:
111
+ # auto_fixable but no registered action — record, do not pretend.
112
+ results.append({"key": comp.key, "success": False,
113
+ "detail": "no repair action registered"})
114
+ failed += 1
115
+ continue
116
+
117
+ cr.mark_transient(comp.key, cr.STATUS_RETRYING, f"repairing {comp.label}…")
118
+ progress(comp.key, f"repairing {comp.label}…")
119
+
120
+ success, detail = False, "not attempted"
121
+ for attempt in range(1, max_retries + 1):
122
+ success, detail = action()
123
+ if success:
124
+ break
125
+ progress(comp.key,
126
+ f"attempt {attempt}/{max_retries} failed: {detail}")
127
+ if attempt < max_retries:
128
+ time.sleep(2)
129
+
130
+ cr.clear_transient(comp.key)
131
+ results.append({"key": comp.key, "success": success, "detail": detail})
132
+ if success:
133
+ healed += 1
134
+ progress(comp.key, f"✓ {detail}")
135
+ else:
136
+ failed += 1
137
+ progress(comp.key, f"✗ {detail} (fix manually: {comp.fix_cmd})")
138
+
139
+ return {
140
+ "attempted": len(targets),
141
+ "healed": healed,
142
+ "failed": failed,
143
+ "results": results,
144
+ }