superlocalmemory 3.6.13 → 3.6.15

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 (147) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/CHANGELOG.md +28 -0
  3. package/README.md +189 -740
  4. package/package.json +12 -5
  5. package/plugin/.claude-plugin/plugin.json +20 -0
  6. package/plugin/.mcp.json +12 -0
  7. package/plugin/CLAUDE.md +44 -0
  8. package/plugin/_GENERATED.md +6 -0
  9. package/plugin/agents/slm-memory-advisor.md +44 -0
  10. package/plugin/agents/slm-optimize-advisor.md +38 -0
  11. package/plugin/hooks/hooks.json +14 -0
  12. package/plugin/requirements.txt +1 -0
  13. package/plugin/scripts/ensure-venv.bat +122 -0
  14. package/plugin/scripts/ensure-venv.sh +105 -0
  15. package/plugin/scripts/slm-launch +15 -0
  16. package/plugin/scripts/slm-launch.bat +17 -0
  17. package/plugin/settings.json +16 -0
  18. package/plugin/skills/slm-cache/SKILL.md +140 -0
  19. package/plugin/skills/slm-compress/SKILL.md +143 -0
  20. package/plugin/skills/slm-graph/SKILL.md +300 -0
  21. package/plugin/skills/slm-recall/SKILL.md +204 -0
  22. package/plugin/skills/slm-remember/SKILL.md +194 -0
  23. package/plugin/skills/slm-session/SKILL.md +207 -0
  24. package/plugin/skills/slm-status/SKILL.md +149 -0
  25. package/plugin-src/.mcp.json +12 -0
  26. package/plugin-src/agents/slm-memory-advisor.md +44 -0
  27. package/plugin-src/agents/slm-optimize-advisor.md +38 -0
  28. package/plugin-src/commands/slm-optimize.md +22 -0
  29. package/plugin-src/commands/slm-recall.md +16 -0
  30. package/plugin-src/commands/slm-remember.md +16 -0
  31. package/plugin-src/commands/slm-status.md +15 -0
  32. package/plugin-src/hooks/.gitkeep +0 -0
  33. package/plugin-src/hooks/hooks.json +14 -0
  34. package/plugin-src/manifest.json +25 -0
  35. package/plugin-src/requirements.txt +1 -0
  36. package/plugin-src/rules/AGENTS.md +91 -0
  37. package/plugin-src/rules/CLAUDE.md.fragment +44 -0
  38. package/plugin-src/scripts/ensure-venv.bat +122 -0
  39. package/plugin-src/scripts/ensure-venv.sh +105 -0
  40. package/plugin-src/scripts/slm-launch +15 -0
  41. package/plugin-src/scripts/slm-launch.bat +17 -0
  42. package/plugin-src/settings.json +16 -0
  43. package/plugin-src/skills/slm-cache/SKILL.md +140 -0
  44. package/plugin-src/skills/slm-compress/SKILL.md +143 -0
  45. package/plugin-src/skills/slm-graph/SKILL.md +300 -0
  46. package/plugin-src/skills/slm-recall/SKILL.md +204 -0
  47. package/plugin-src/skills/slm-remember/SKILL.md +194 -0
  48. package/plugin-src/skills/slm-session/SKILL.md +207 -0
  49. package/plugin-src/skills/slm-status/SKILL.md +149 -0
  50. package/pyproject.toml +6 -2
  51. package/scripts/__tests__/build-plugin.test.mjs +613 -0
  52. package/scripts/_savings_math.py +270 -0
  53. package/scripts/build-plugin.js +742 -0
  54. package/scripts/dogfood_savings.py +490 -0
  55. package/scripts/install-skills.ps1 +4 -334
  56. package/scripts/install-skills.sh +4 -435
  57. package/scripts/postinstall-interactive.js +0 -27
  58. package/scripts/postinstall.js +21 -2
  59. package/src/superlocalmemory/__init__.py +1 -1
  60. package/src/superlocalmemory/cli/_lazy_init.py +115 -0
  61. package/src/superlocalmemory/cli/commands.py +439 -41
  62. package/src/superlocalmemory/cli/main.py +92 -4
  63. package/src/superlocalmemory/cli/setup_wizard.py +47 -6
  64. package/src/superlocalmemory/core/backend_orchestrator.py +12 -8
  65. package/src/superlocalmemory/core/config.py +194 -9
  66. package/src/superlocalmemory/core/embeddings.py +10 -5
  67. package/src/superlocalmemory/core/engine.py +76 -5
  68. package/src/superlocalmemory/core/fact_consolidator.py +20 -3
  69. package/src/superlocalmemory/core/platform_utils.py +8 -0
  70. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  71. package/src/superlocalmemory/core/recall_worker.py +7 -0
  72. package/src/superlocalmemory/core/store_pipeline.py +23 -1
  73. package/src/superlocalmemory/core/worker_pool.py +14 -2
  74. package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
  75. package/src/superlocalmemory/hooks/portable_kit.py +506 -0
  76. package/src/superlocalmemory/hooks/session_registry.py +8 -4
  77. package/src/superlocalmemory/infra/cloud_backup.py +99 -23
  78. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -2
  79. package/src/superlocalmemory/mcp/_pool_adapter.py +15 -6
  80. package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
  81. package/src/superlocalmemory/mcp/server.py +75 -4
  82. package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
  83. package/src/superlocalmemory/mcp/tools_core.py +37 -4
  84. package/src/superlocalmemory/mcp/tools_v3.py +6 -1
  85. package/src/superlocalmemory/mcp/tools_v33.py +8 -4
  86. package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
  87. package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
  88. package/src/superlocalmemory/optimize/cache/manager.py +92 -6
  89. package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
  90. package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
  91. package/src/superlocalmemory/optimize/compress/router.py +46 -13
  92. package/src/superlocalmemory/optimize/config/schema.py +6 -0
  93. package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
  94. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
  95. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
  96. package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
  97. package/src/superlocalmemory/optimize/proxy/server.py +11 -0
  98. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
  99. package/src/superlocalmemory/optimize/storage/db.py +30 -0
  100. package/src/superlocalmemory/retrieval/bm25_channel.py +12 -2
  101. package/src/superlocalmemory/retrieval/engine.py +36 -3
  102. package/src/superlocalmemory/retrieval/entity_channel.py +5 -5
  103. package/src/superlocalmemory/retrieval/hopfield_channel.py +10 -2
  104. package/src/superlocalmemory/retrieval/semantic_channel.py +10 -2
  105. package/src/superlocalmemory/server/recall_serializer.py +3 -1
  106. package/src/superlocalmemory/server/unified_daemon.py +156 -16
  107. package/src/superlocalmemory/storage/database.py +215 -43
  108. package/src/superlocalmemory/storage/migration_runner.py +17 -1
  109. package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +120 -0
  110. package/src/superlocalmemory/storage/models.py +10 -0
  111. package/src/superlocalmemory/storage/schema.py +15 -10
  112. package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
  113. package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
  114. package/src/superlocalmemory/ui/index.html +2 -2
  115. package/src/superlocalmemory/ui/js/core.js +98 -0
  116. package/src/superlocalmemory/ui/js/dashboard.js +8 -1
  117. package/src/superlocalmemory/ui/js/ide-status.js +16 -3
  118. package/src/superlocalmemory/ui/js/math-health.js +15 -3
  119. package/src/superlocalmemory/ui/js/optimize.js +18 -2
  120. package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
  121. package/src/superlocalmemory.egg-info/PKG-INFO +191 -741
  122. package/src/superlocalmemory.egg-info/SOURCES.txt +7 -9
  123. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  124. package/ide/skills/slm-build-graph/SKILL.md +0 -423
  125. package/ide/skills/slm-list-recent/SKILL.md +0 -348
  126. package/ide/skills/slm-recall/SKILL.md +0 -326
  127. package/ide/skills/slm-remember/SKILL.md +0 -194
  128. package/ide/skills/slm-show-patterns/SKILL.md +0 -224
  129. package/ide/skills/slm-status/SKILL.md +0 -363
  130. package/ide/skills/slm-switch-profile/SKILL.md +0 -442
  131. package/skills/slm-build-graph/SKILL.md +0 -423
  132. package/skills/slm-list-recent/SKILL.md +0 -348
  133. package/skills/slm-optimize/README.md +0 -55
  134. package/skills/slm-optimize/SKILL.md +0 -139
  135. package/skills/slm-recall/SKILL.md +0 -343
  136. package/skills/slm-remember/SKILL.md +0 -194
  137. package/skills/slm-show-patterns/SKILL.md +0 -224
  138. package/skills/slm-status/SKILL.md +0 -363
  139. package/skills/slm-switch-profile/SKILL.md +0 -442
  140. package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
  141. package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
  142. package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
  143. package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
  144. package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
  145. package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
  146. package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
  147. package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +0 -442
@@ -18,7 +18,9 @@ from __future__ import annotations
18
18
 
19
19
  import json
20
20
  import logging
21
+ import os
21
22
  import sqlite3
23
+ import tempfile
22
24
  from datetime import datetime, UTC, timezone
23
25
  from pathlib import Path
24
26
  from typing import Any
@@ -29,42 +31,103 @@ MEMORY_DIR = Path.home() / ".superlocalmemory"
29
31
  DB_PATH = MEMORY_DIR / "memory.db"
30
32
  KEYRING_SERVICE = "superlocalmemory"
31
33
 
34
+ # Stage-9 fix: hoist NoKeyringError to module scope. Previously each function
35
+ # did `from keyring.errors import NoKeyringError` INSIDE the same try whose
36
+ # `import keyring` could fail — leaving the name unbound when the `except
37
+ # (ImportError, NoKeyringError)` tuple was evaluated, which raised
38
+ # UnboundLocalError on every keyring-free host (headless Linux / minimal
39
+ # Docker) and made the plaintext fallback unreachable. A sentinel subclass is
40
+ # used when keyring is absent so the except-tuple is always valid and simply
41
+ # never matches.
42
+ try:
43
+ from keyring.errors import NoKeyringError as _NoKeyringError
44
+ except Exception: # keyring not installed at all
45
+ class _NoKeyringError(Exception):
46
+ """Sentinel — keyring unavailable; this is never raised."""
47
+
32
48
  # ---------------------------------------------------------------------------
33
49
  # Credential management (OS keychain)
34
50
  # ---------------------------------------------------------------------------
35
51
 
36
52
 
37
53
  def _get_credential_store() -> Path:
38
- """Fallback encrypted credential file for systems without a keychain."""
54
+ """Fallback plaintext-local-file credential store for systems without a keychain."""
39
55
  return MEMORY_DIR / ".credentials.json"
40
56
 
41
57
 
58
+ def _atomic_write_creds(store_path: Path, data: dict) -> None:
59
+ """Write credential data atomically at 0o600 from creation.
60
+
61
+ Uses os.open with O_CREAT|O_WRONLY|O_TRUNC|O_NOFOLLOW and mode 0o600 so
62
+ the file is never world-readable — not even for an instant. The payload
63
+ is written to a temp file in the SAME directory (guaranteeing same device),
64
+ fsynced, and then renamed into place via os.replace() so the old store
65
+ survives any crash between write and rename (atomic on POSIX).
66
+
67
+ Mirrors the pattern used in optimize/proxy/capture.py:111-112.
68
+ """
69
+ parent = store_path.parent
70
+ parent.mkdir(parents=True, exist_ok=True)
71
+ # Ensure the directory itself is private — 0o700 (owner rwx only)
72
+ try:
73
+ os.chmod(parent, 0o700)
74
+ except OSError:
75
+ pass # Best-effort; existing dirs may be fine already
76
+
77
+ payload = json.dumps(data).encode()
78
+
79
+ # Write to a temp file in the SAME directory (same filesystem → atomic rename)
80
+ tmp_fd, tmp_name = tempfile.mkstemp(dir=parent, prefix=".creds-")
81
+ try:
82
+ # Re-open with 0o600; mkstemp already creates with 0o600 on POSIX.
83
+ # Use os.open with O_NOFOLLOW on the temp name to refuse symlink tricks.
84
+ os.close(tmp_fd)
85
+ flags = os.O_WRONLY | os.O_TRUNC | getattr(os, "O_NOFOLLOW", 0)
86
+ fd = os.open(tmp_name, flags, 0o600)
87
+ try:
88
+ os.write(fd, payload)
89
+ os.fsync(fd)
90
+ finally:
91
+ os.close(fd)
92
+ os.replace(tmp_name, store_path)
93
+ except Exception:
94
+ # Clean up the temp file if rename failed; let caller log & re-raise
95
+ try:
96
+ os.unlink(tmp_name)
97
+ except FileNotFoundError:
98
+ pass
99
+ raise
100
+
101
+
42
102
  def _store_credential(key: str, value: str) -> bool:
43
103
  """Store a credential in the OS keychain (macOS/Windows/Linux).
44
104
 
45
- Falls back to an encrypted local file on headless Linux or if
46
- the keyring backend is unavailable.
105
+ Falls back to a plaintext local file (owner read/write only, 0o600) on
106
+ headless Linux or if the keyring backend is unavailable.
47
107
  """
48
108
  # Try OS keychain first (macOS Keychain, Windows Credential Locker, Linux SecretService)
49
109
  try:
50
110
  import keyring
51
- from keyring.errors import NoKeyringError
52
111
  keyring.set_password(KEYRING_SERVICE, key, value)
53
112
  return True
54
- except (ImportError, NoKeyringError):
113
+ except (ImportError, _NoKeyringError):
55
114
  pass # No keyring backend — use fallback
56
115
  except Exception as exc:
57
116
  logger.debug("Keyring store failed, using fallback: %s", exc)
58
117
 
59
- # Fallback: local file with restricted permissions (0600)
118
+ # Fallback: plaintext local file, atomic write at 0o600 from creation
60
119
  try:
61
120
  store_path = _get_credential_store()
62
- existing = {}
121
+ existing: dict = {}
63
122
  if store_path.exists():
64
- existing = json.loads(store_path.read_text())
123
+ try:
124
+ existing = json.loads(store_path.read_text())
125
+ except json.JSONDecodeError as exc:
126
+ logger.warning(
127
+ "Credential store corrupt (JSON decode error), starting fresh: %s", exc
128
+ )
65
129
  existing[key] = value
66
- store_path.write_text(json.dumps(existing))
67
- store_path.chmod(0o600) # Owner read/write only
130
+ _atomic_write_creds(store_path, existing)
68
131
  return True
69
132
  except Exception as exc:
70
133
  logger.warning("Failed to store credential '%s': %s", key, exc)
@@ -76,21 +139,28 @@ def _get_credential(key: str) -> str | None:
76
139
  # Try OS keychain first
77
140
  try:
78
141
  import keyring
79
- from keyring.errors import NoKeyringError
80
142
  val = keyring.get_password(KEYRING_SERVICE, key)
81
143
  if val is not None:
82
144
  return val
83
- except (ImportError, NoKeyringError):
145
+ except (ImportError, _NoKeyringError):
84
146
  pass
85
147
  except Exception:
86
148
  pass
87
149
 
88
- # Fallback: local file
150
+ # Fallback: plaintext local file
89
151
  try:
90
152
  store_path = _get_credential_store()
91
153
  if store_path.exists():
92
- data = json.loads(store_path.read_text())
93
- return data.get(key)
154
+ try:
155
+ data = json.loads(store_path.read_text())
156
+ return data.get(key)
157
+ except json.JSONDecodeError as exc:
158
+ logger.warning(
159
+ "Credential store corrupt (JSON decode error), cannot read key '%s': %s",
160
+ key,
161
+ exc,
162
+ )
163
+ return None
94
164
  except Exception:
95
165
  pass
96
166
 
@@ -103,26 +173,32 @@ def _delete_credential(key: str) -> bool:
103
173
 
104
174
  try:
105
175
  import keyring
106
- from keyring.errors import NoKeyringError
107
176
  keyring.delete_password(KEYRING_SERVICE, key)
108
177
  deleted = True
109
- except (ImportError, NoKeyringError):
178
+ except (ImportError, _NoKeyringError):
110
179
  pass
111
180
  except Exception:
112
181
  pass
113
182
 
114
- # Also clean from fallback
183
+ # Also clean from fallback store — atomic write at 0o600
115
184
  try:
116
185
  store_path = _get_credential_store()
117
186
  if store_path.exists():
118
- data = json.loads(store_path.read_text())
187
+ try:
188
+ data = json.loads(store_path.read_text())
189
+ except json.JSONDecodeError as exc:
190
+ logger.warning(
191
+ "Credential store corrupt (JSON decode error) during delete of '%s': %s",
192
+ key,
193
+ exc,
194
+ )
195
+ return deleted
119
196
  if key in data:
120
197
  del data[key]
121
- store_path.write_text(json.dumps(data))
122
- store_path.chmod(0o600)
198
+ _atomic_write_creds(store_path, data)
123
199
  deleted = True
124
- except Exception:
125
- pass
200
+ except Exception as exc:
201
+ logger.warning("Failed to delete credential '%s' from fallback store: %s", key, exc)
126
202
 
127
203
  return deleted
128
204
 
@@ -46,13 +46,23 @@ class DaemonPoolProxy:
46
46
  def recall(
47
47
  self, query: str, limit: int = 10, session_id: str = "",
48
48
  fast: bool = False,
49
+ include_global: bool | None = None,
50
+ include_shared: bool | None = None,
49
51
  ) -> dict[str, Any]:
50
- params = urllib.parse.urlencode({
52
+ _params: dict[str, Any] = {
51
53
  "q": query,
52
54
  "limit": limit,
53
55
  "session_id": session_id or "",
54
56
  "fast": "true" if fast else "false",
55
- })
57
+ }
58
+ # v3.6.15 multi-scope: only send the scope flags when explicitly set, so
59
+ # an unset value lets the daemon resolve the configured default (shared
60
+ # is opt-in). "None" must NOT become the string "none" on the wire.
61
+ if include_global is not None:
62
+ _params["include_global"] = "true" if include_global else "false"
63
+ if include_shared is not None:
64
+ _params["include_shared"] = "true" if include_shared else "false"
65
+ params = urllib.parse.urlencode(_params)
56
66
  try:
57
67
  with urllib.request.urlopen(
58
68
  self._url(f"/recall?{params}"), timeout=self._timeout,
@@ -80,12 +80,21 @@ def pool_recall(query: str, limit: int = 10, **kwargs: Any) -> PoolRecallRespons
80
80
 
81
81
  Raises :class:`PoolError` on worker death or any non-ok envelope.
82
82
  """
83
- raw = _pool().recall(
84
- query=query,
85
- limit=limit,
86
- session_id=str(kwargs.get("session_id") or ""),
87
- fast=bool(kwargs.get("fast", False)),
88
- )
83
+ # v3.6.15 multi-scope: forward the scope-visibility flags when the caller
84
+ # set them. ``None`` (the default) is passed through so the daemon/engine
85
+ # resolves the configured default — shared memory is opt-in, so omitting
86
+ # them keeps recall scoped to this profile only.
87
+ _recall_kwargs: dict[str, Any] = {
88
+ "query": query,
89
+ "limit": limit,
90
+ "session_id": str(kwargs.get("session_id") or ""),
91
+ "fast": bool(kwargs.get("fast", False)),
92
+ }
93
+ if "include_global" in kwargs:
94
+ _recall_kwargs["include_global"] = kwargs["include_global"]
95
+ if "include_shared" in kwargs:
96
+ _recall_kwargs["include_shared"] = kwargs["include_shared"]
97
+ raw = _pool().recall(**_recall_kwargs)
89
98
  _unwrap_error(raw, "recall")
90
99
  items = raw.get("results", []) if isinstance(raw, dict) else []
91
100
  results = [