superlocalmemory 3.6.13 → 3.6.14

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 (124) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/README.md +187 -741
  3. package/package.json +12 -5
  4. package/plugin/.claude-plugin/plugin.json +20 -0
  5. package/plugin/.mcp.json +12 -0
  6. package/plugin/CLAUDE.md +43 -0
  7. package/plugin/_GENERATED.md +6 -0
  8. package/plugin/agents/slm-memory-advisor.md +43 -0
  9. package/plugin/agents/slm-optimize-advisor.md +38 -0
  10. package/plugin/hooks/hooks.json +14 -0
  11. package/plugin/requirements.txt +1 -0
  12. package/plugin/scripts/ensure-venv.bat +122 -0
  13. package/plugin/scripts/ensure-venv.sh +105 -0
  14. package/plugin/scripts/slm-launch +15 -0
  15. package/plugin/scripts/slm-launch.bat +17 -0
  16. package/plugin/settings.json +16 -0
  17. package/plugin/skills/slm-cache/SKILL.md +140 -0
  18. package/plugin/skills/slm-compress/SKILL.md +143 -0
  19. package/plugin/skills/slm-graph/SKILL.md +300 -0
  20. package/plugin/skills/slm-recall/SKILL.md +196 -0
  21. package/plugin/skills/slm-remember/SKILL.md +182 -0
  22. package/plugin/skills/slm-session/SKILL.md +207 -0
  23. package/plugin/skills/slm-status/SKILL.md +149 -0
  24. package/plugin-src/.mcp.json +12 -0
  25. package/plugin-src/agents/slm-memory-advisor.md +43 -0
  26. package/plugin-src/agents/slm-optimize-advisor.md +38 -0
  27. package/plugin-src/commands/slm-optimize.md +22 -0
  28. package/plugin-src/commands/slm-recall.md +16 -0
  29. package/plugin-src/commands/slm-remember.md +16 -0
  30. package/plugin-src/commands/slm-status.md +15 -0
  31. package/plugin-src/hooks/.gitkeep +0 -0
  32. package/plugin-src/hooks/hooks.json +14 -0
  33. package/plugin-src/manifest.json +25 -0
  34. package/plugin-src/requirements.txt +1 -0
  35. package/plugin-src/rules/AGENTS.md +90 -0
  36. package/plugin-src/rules/CLAUDE.md.fragment +43 -0
  37. package/plugin-src/scripts/ensure-venv.bat +122 -0
  38. package/plugin-src/scripts/ensure-venv.sh +105 -0
  39. package/plugin-src/scripts/slm-launch +15 -0
  40. package/plugin-src/scripts/slm-launch.bat +17 -0
  41. package/plugin-src/settings.json +16 -0
  42. package/plugin-src/skills/slm-cache/SKILL.md +140 -0
  43. package/plugin-src/skills/slm-compress/SKILL.md +143 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +300 -0
  45. package/plugin-src/skills/slm-recall/SKILL.md +196 -0
  46. package/plugin-src/skills/slm-remember/SKILL.md +182 -0
  47. package/plugin-src/skills/slm-session/SKILL.md +207 -0
  48. package/plugin-src/skills/slm-status/SKILL.md +149 -0
  49. package/pyproject.toml +6 -2
  50. package/scripts/__tests__/build-plugin.test.mjs +613 -0
  51. package/scripts/_savings_math.py +270 -0
  52. package/scripts/build-plugin.js +742 -0
  53. package/scripts/dogfood_savings.py +490 -0
  54. package/scripts/install-skills.ps1 +4 -334
  55. package/scripts/install-skills.sh +4 -435
  56. package/scripts/postinstall-interactive.js +0 -27
  57. package/scripts/postinstall.js +21 -2
  58. package/src/superlocalmemory/__init__.py +1 -1
  59. package/src/superlocalmemory/cli/_lazy_init.py +115 -0
  60. package/src/superlocalmemory/cli/commands.py +348 -39
  61. package/src/superlocalmemory/cli/main.py +47 -4
  62. package/src/superlocalmemory/cli/setup_wizard.py +20 -6
  63. package/src/superlocalmemory/core/config.py +79 -9
  64. package/src/superlocalmemory/core/embeddings.py +10 -5
  65. package/src/superlocalmemory/core/engine.py +2 -2
  66. package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
  67. package/src/superlocalmemory/hooks/portable_kit.py +506 -0
  68. package/src/superlocalmemory/infra/cloud_backup.py +99 -23
  69. package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
  70. package/src/superlocalmemory/mcp/server.py +75 -4
  71. package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
  72. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  73. package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
  74. package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
  75. package/src/superlocalmemory/optimize/cache/manager.py +92 -6
  76. package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
  77. package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
  78. package/src/superlocalmemory/optimize/compress/router.py +46 -13
  79. package/src/superlocalmemory/optimize/config/schema.py +6 -0
  80. package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
  81. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
  82. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
  83. package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
  84. package/src/superlocalmemory/optimize/proxy/server.py +11 -0
  85. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
  86. package/src/superlocalmemory/optimize/storage/db.py +30 -0
  87. package/src/superlocalmemory/server/recall_serializer.py +3 -1
  88. package/src/superlocalmemory/server/unified_daemon.py +24 -6
  89. package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
  90. package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
  91. package/src/superlocalmemory/ui/index.html +2 -2
  92. package/src/superlocalmemory/ui/js/core.js +98 -0
  93. package/src/superlocalmemory/ui/js/dashboard.js +8 -1
  94. package/src/superlocalmemory/ui/js/ide-status.js +16 -3
  95. package/src/superlocalmemory/ui/js/math-health.js +15 -3
  96. package/src/superlocalmemory/ui/js/optimize.js +18 -2
  97. package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
  98. package/src/superlocalmemory.egg-info/PKG-INFO +189 -742
  99. package/src/superlocalmemory.egg-info/SOURCES.txt +6 -9
  100. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  101. package/ide/skills/slm-build-graph/SKILL.md +0 -423
  102. package/ide/skills/slm-list-recent/SKILL.md +0 -348
  103. package/ide/skills/slm-recall/SKILL.md +0 -326
  104. package/ide/skills/slm-remember/SKILL.md +0 -194
  105. package/ide/skills/slm-show-patterns/SKILL.md +0 -224
  106. package/ide/skills/slm-status/SKILL.md +0 -363
  107. package/ide/skills/slm-switch-profile/SKILL.md +0 -442
  108. package/skills/slm-build-graph/SKILL.md +0 -423
  109. package/skills/slm-list-recent/SKILL.md +0 -348
  110. package/skills/slm-optimize/README.md +0 -55
  111. package/skills/slm-optimize/SKILL.md +0 -139
  112. package/skills/slm-recall/SKILL.md +0 -343
  113. package/skills/slm-remember/SKILL.md +0 -194
  114. package/skills/slm-show-patterns/SKILL.md +0 -224
  115. package/skills/slm-status/SKILL.md +0 -363
  116. package/skills/slm-switch-profile/SKILL.md +0 -442
  117. package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
  118. package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
  119. package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
  120. package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
  121. package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
  122. package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
  123. package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
  124. package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +0 -442
@@ -21,6 +21,16 @@ logger = logging.getLogger(__name__)
21
21
  from superlocalmemory.storage.models import Mode
22
22
 
23
23
 
24
+ # ---------------------------------------------------------------------------
25
+ # Canonical limits (WP-02 — single source of truth across all surfaces)
26
+ # ---------------------------------------------------------------------------
27
+
28
+ #: Default number of results returned by recall across MCP, CLI, daemon, and
29
+ #: engine. All surfaces bind their ``limit`` defaults to this constant so a
30
+ #: single change is sufficient to keep the full stack in sync.
31
+ CANONICAL_RECALL_LIMIT: int = 20
32
+
33
+
24
34
  # ---------------------------------------------------------------------------
25
35
  # Default Paths
26
36
  # ---------------------------------------------------------------------------
@@ -739,17 +749,32 @@ class SLMConfig:
739
749
  @classmethod
740
750
  def load(cls, config_path: Path | None = None) -> SLMConfig:
741
751
  """Load config from JSON file. Returns default Mode A if file doesn't exist."""
742
- path = config_path or (DEFAULT_BASE_DIR / "config.json")
752
+ # WP-07: resolve base dir through slm_home() at call time so all 3 env
753
+ # aliases (SLM_DATA_DIR → SL_MEMORY_PATH → SLM_HOME) are honoured.
754
+ try:
755
+ from superlocalmemory.cli._lazy_init import slm_home as _slm_home
756
+ _runtime_base = _slm_home()
757
+ except Exception:
758
+ _runtime_base = DEFAULT_BASE_DIR
759
+ path = config_path or (_runtime_base / "config.json")
743
760
  if not path.exists():
744
761
  return cls.for_mode(Mode.A)
745
762
  import json
746
- data = json.loads(path.read_text())
763
+ try:
764
+ data = json.loads(path.read_text())
765
+ except (json.JSONDecodeError, OSError) as exc:
766
+ # An already-corrupt/truncated config.json must NOT brick every `slm`
767
+ # call — degrade to the Mode-A default and warn rather than raise.
768
+ logger.warning(
769
+ "config.json unreadable/corrupt (%s) — using Mode A default", exc
770
+ )
771
+ return cls.for_mode(Mode.A)
747
772
  mode = Mode(data.get("mode", "a"))
748
773
  llm_data = data.get("llm", {})
749
774
  emb_data = data.get("embedding", {})
750
775
  # V3.5.9: read base_dir before constructing config so for_mode() builds
751
776
  # db_path from the user's directory, not DEFAULT_BASE_DIR.
752
- raw_base_dir = Path(data.get("base_dir", str(DEFAULT_BASE_DIR)))
777
+ raw_base_dir = Path(data.get("base_dir", str(_runtime_base)))
753
778
  config = cls.for_mode(
754
779
  mode,
755
780
  llm_provider=llm_data.get("provider", ""),
@@ -932,7 +957,12 @@ class SLMConfig:
932
957
  if key in existing:
933
958
  data[key] = existing[key]
934
959
 
935
- path.write_text(json.dumps(data, indent=2))
960
+ # Atomic write: a crash mid-write must NOT leave a truncated/corrupt
961
+ # config.json (which would make every subsequent `slm` call fail to load).
962
+ import os as _os
963
+ _tmp = path.with_suffix(path.suffix + ".tmp")
964
+ _tmp.write_text(json.dumps(data, indent=2))
965
+ _os.replace(_tmp, path)
936
966
 
937
967
  @staticmethod
938
968
  def provider_presets() -> dict[str, dict[str, str]]:
@@ -987,7 +1017,15 @@ class SLMConfig:
987
1017
  embedding_dimension: int = 0,
988
1018
  ) -> SLMConfig:
989
1019
  """Create config with mode-appropriate defaults."""
990
- _base = base_dir or DEFAULT_BASE_DIR
1020
+ # WP-07: resolve base dir via slm_home() at call time when not explicit.
1021
+ if base_dir is None:
1022
+ try:
1023
+ from superlocalmemory.cli._lazy_init import slm_home as _slm_home
1024
+ _base = _slm_home()
1025
+ except Exception:
1026
+ _base = DEFAULT_BASE_DIR
1027
+ else:
1028
+ _base = base_dir
991
1029
 
992
1030
  if mode == Mode.A:
993
1031
  # V3.4.24: If user chose "openai" provider, honour their custom
@@ -1130,7 +1168,15 @@ class SLMConfig:
1130
1168
 
1131
1169
  Returns ``\"b\"`` (the default) if the file doesn't exist.
1132
1170
  """
1133
- _base = base_dir or DEFAULT_BASE_DIR
1171
+ # WP-07: resolve via slm_home() at call time when base_dir not explicit.
1172
+ if base_dir is None:
1173
+ try:
1174
+ from superlocalmemory.cli._lazy_init import slm_home as _slm_home
1175
+ _base = _slm_home()
1176
+ except Exception:
1177
+ _base = DEFAULT_BASE_DIR
1178
+ else:
1179
+ _base = base_dir
1134
1180
  _f = _base / CURRENT_MODE_FILE
1135
1181
  try:
1136
1182
  return _f.read_text(encoding="utf-8").strip().lower() or "b"
@@ -1140,7 +1186,15 @@ class SLMConfig:
1140
1186
  @staticmethod
1141
1187
  def write_current_mode(mode: str, base_dir: Path | None = None) -> None:
1142
1188
  """Write the current mode letter to ``current_mode``."""
1143
- _base = base_dir or DEFAULT_BASE_DIR
1189
+ # WP-07: resolve via slm_home() at call time when base_dir not explicit.
1190
+ if base_dir is None:
1191
+ try:
1192
+ from superlocalmemory.cli._lazy_init import slm_home as _slm_home
1193
+ _base = _slm_home()
1194
+ except Exception:
1195
+ _base = DEFAULT_BASE_DIR
1196
+ else:
1197
+ _base = base_dir
1144
1198
  _base.mkdir(parents=True, exist_ok=True)
1145
1199
  (_base / CURRENT_MODE_FILE).write_text(
1146
1200
  mode.lower().strip(), encoding="utf-8",
@@ -1175,7 +1229,15 @@ class SLMConfig:
1175
1229
  import dataclasses
1176
1230
 
1177
1231
  from superlocalmemory.storage.models import Mode as _M
1178
- _base = base_dir or DEFAULT_BASE_DIR
1232
+ # WP-07: resolve via slm_home() at call time when base_dir not explicit.
1233
+ if base_dir is None:
1234
+ try:
1235
+ from superlocalmemory.cli._lazy_init import slm_home as _slm_home
1236
+ _base = _slm_home()
1237
+ except Exception:
1238
+ _base = DEFAULT_BASE_DIR
1239
+ else:
1240
+ _base = base_dir
1179
1241
  _base.mkdir(parents=True, exist_ok=True)
1180
1242
 
1181
1243
  old_config = cls.load(_base / "config.json")
@@ -1242,7 +1304,15 @@ class SLMConfig:
1242
1304
  Called on daemon boot. Idempotent — if ``current_mode`` already
1243
1305
  exists, this is a no-op. Returns True if migration was performed.
1244
1306
  """
1245
- _base = base_dir or DEFAULT_BASE_DIR
1307
+ # WP-07: resolve via slm_home() at call time when base_dir not explicit.
1308
+ if base_dir is None:
1309
+ try:
1310
+ from superlocalmemory.cli._lazy_init import slm_home as _slm_home
1311
+ _base = _slm_home()
1312
+ except Exception:
1313
+ _base = DEFAULT_BASE_DIR
1314
+ else:
1315
+ _base = base_dir
1246
1316
  _current = _base / CURRENT_MODE_FILE
1247
1317
  if _current.exists():
1248
1318
  return False # already migrated
@@ -601,8 +601,13 @@ class EmbeddingService:
601
601
  def _cloud_embed_batch(
602
602
  self, texts: list[str], *, max_retries: int = 3,
603
603
  ) -> list[list[float]]:
604
- """Encode via Azure OpenAI embedding API with retry."""
605
- import httpx
604
+ """Encode via Azure OpenAI embedding API with retry.
605
+
606
+ V3.6.14: Reuses self._get_http_client() (shared persistent connection)
607
+ instead of creating a fresh httpx.Client per call/retry. resp.json()
608
+ is now called inside the try block while the response object is still
609
+ in scope, eliminating reliance on httpx body-buffering after close.
610
+ """
606
611
  url = (
607
612
  f"{self._config.api_endpoint.rstrip('/')}/openai/deployments/"
608
613
  f"{self._config.deployment_name}/embeddings"
@@ -613,12 +618,12 @@ class EmbeddingService:
613
618
  "api-key": self._config.api_key,
614
619
  }
615
620
  body = {"input": texts, "model": self._config.deployment_name}
621
+ client = self._get_http_client()
616
622
  last_error: Exception | None = None
617
623
  for attempt in range(max_retries):
618
624
  try:
619
- with httpx.Client(timeout=httpx.Timeout(30.0)) as client:
620
- resp = client.post(url, headers=headers, json=body)
621
- resp.raise_for_status()
625
+ resp = client.post(url, headers=headers, json=body)
626
+ resp.raise_for_status()
622
627
  data = resp.json()
623
628
  results = []
624
629
  for item in sorted(data["data"], key=lambda d: d["index"]):
@@ -20,7 +20,7 @@ from __future__ import annotations
20
20
  import logging
21
21
  from typing import Any
22
22
 
23
- from superlocalmemory.core.config import SLMConfig
23
+ from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT, SLMConfig
24
24
  from superlocalmemory.core.engine_capabilities import Capabilities, CapabilityError
25
25
  from superlocalmemory.core.modes import get_capabilities
26
26
  from superlocalmemory.storage.models import (
@@ -507,7 +507,7 @@ class MemoryEngine:
507
507
 
508
508
  def recall(
509
509
  self, query: str, profile_id: str | None = None,
510
- mode: Mode | None = None, limit: int = 20,
510
+ mode: Mode | None = None, limit: int = CANONICAL_RECALL_LIMIT,
511
511
  agent_id: str = "unknown",
512
512
  session_id: str | None = None,
513
513
  fast: bool = False,
@@ -266,7 +266,12 @@ def _remove_slm_hooks(settings: dict) -> dict:
266
266
 
267
267
 
268
268
  def _read_settings() -> dict:
269
- """Read Claude Code settings.json, return empty dict if missing."""
269
+ """Read Claude Code settings.json, return empty dict if missing.
270
+
271
+ Raises:
272
+ json.JSONDecodeError: propagated as-is when the file exists but is
273
+ malformed, so callers can distinguish 'missing' from 'corrupt'.
274
+ """
270
275
  if CLAUDE_SETTINGS.exists():
271
276
  return json.loads(CLAUDE_SETTINGS.read_text())
272
277
  return {}
@@ -356,6 +361,8 @@ def check_status() -> dict:
356
361
 
357
362
  hook_types_found: list[str] = []
358
363
  has_gate = False
364
+ parse_error: str | None = None
365
+
359
366
  try:
360
367
  settings = _read_settings()
361
368
  for hook_type, entries in settings.get("hooks", {}).items():
@@ -373,12 +380,26 @@ def check_status() -> dict:
373
380
  break
374
381
  if has_gate:
375
382
  break
383
+ except json.JSONDecodeError as exc:
384
+ # File exists but is malformed — report indeterminate state so callers
385
+ # do not conflate 'corrupt' with 'not installed'. Claude Code reads the
386
+ # file independently; hooks may still be firing even though we cannot
387
+ # parse the file here (split-brain scenario).
388
+ parse_error = f"JSON parse error in settings.json: {exc}"
389
+ logger.warning("SLM: %s", parse_error)
376
390
  except Exception:
377
391
  pass
378
392
 
379
- installed = len(hook_types_found) >= 3
393
+ # Three-valued installed:
394
+ # True — enough SLM hook types found (≥3)
395
+ # False — file missing or parseable but no SLM hooks
396
+ # None — file exists but is corrupt (indeterminate)
397
+ if parse_error is not None:
398
+ installed: bool | None = None
399
+ else:
400
+ installed = len(hook_types_found) >= 3
380
401
 
381
- return {
402
+ result: dict = {
382
403
  "installed": installed,
383
404
  "version": installed_version,
384
405
  "latest_version": HOOKS_VERSION,
@@ -386,6 +407,9 @@ def check_status() -> dict:
386
407
  "hook_types": hook_types_found,
387
408
  "gate_enabled": has_gate,
388
409
  }
410
+ if parse_error is not None:
411
+ result["error"] = parse_error
412
+ return result
389
413
 
390
414
 
391
415
  def upgrade_hooks() -> dict: