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.
- package/.claude-plugin/marketplace.json +17 -0
- package/CHANGELOG.md +28 -0
- package/README.md +189 -740
- package/package.json +12 -5
- package/plugin/.claude-plugin/plugin.json +20 -0
- package/plugin/.mcp.json +12 -0
- package/plugin/CLAUDE.md +44 -0
- package/plugin/_GENERATED.md +6 -0
- package/plugin/agents/slm-memory-advisor.md +44 -0
- package/plugin/agents/slm-optimize-advisor.md +38 -0
- package/plugin/hooks/hooks.json +14 -0
- package/plugin/requirements.txt +1 -0
- package/plugin/scripts/ensure-venv.bat +122 -0
- package/plugin/scripts/ensure-venv.sh +105 -0
- package/plugin/scripts/slm-launch +15 -0
- package/plugin/scripts/slm-launch.bat +17 -0
- package/plugin/settings.json +16 -0
- package/plugin/skills/slm-cache/SKILL.md +140 -0
- package/plugin/skills/slm-compress/SKILL.md +143 -0
- package/plugin/skills/slm-graph/SKILL.md +300 -0
- package/plugin/skills/slm-recall/SKILL.md +204 -0
- package/plugin/skills/slm-remember/SKILL.md +194 -0
- package/plugin/skills/slm-session/SKILL.md +207 -0
- package/plugin/skills/slm-status/SKILL.md +149 -0
- package/plugin-src/.mcp.json +12 -0
- package/plugin-src/agents/slm-memory-advisor.md +44 -0
- package/plugin-src/agents/slm-optimize-advisor.md +38 -0
- package/plugin-src/commands/slm-optimize.md +22 -0
- package/plugin-src/commands/slm-recall.md +16 -0
- package/plugin-src/commands/slm-remember.md +16 -0
- package/plugin-src/commands/slm-status.md +15 -0
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +14 -0
- package/plugin-src/manifest.json +25 -0
- package/plugin-src/requirements.txt +1 -0
- package/plugin-src/rules/AGENTS.md +91 -0
- package/plugin-src/rules/CLAUDE.md.fragment +44 -0
- package/plugin-src/scripts/ensure-venv.bat +122 -0
- package/plugin-src/scripts/ensure-venv.sh +105 -0
- package/plugin-src/scripts/slm-launch +15 -0
- package/plugin-src/scripts/slm-launch.bat +17 -0
- package/plugin-src/settings.json +16 -0
- package/plugin-src/skills/slm-cache/SKILL.md +140 -0
- package/plugin-src/skills/slm-compress/SKILL.md +143 -0
- package/plugin-src/skills/slm-graph/SKILL.md +300 -0
- package/plugin-src/skills/slm-recall/SKILL.md +204 -0
- package/plugin-src/skills/slm-remember/SKILL.md +194 -0
- package/plugin-src/skills/slm-session/SKILL.md +207 -0
- package/plugin-src/skills/slm-status/SKILL.md +149 -0
- package/pyproject.toml +6 -2
- package/scripts/__tests__/build-plugin.test.mjs +613 -0
- package/scripts/_savings_math.py +270 -0
- package/scripts/build-plugin.js +742 -0
- package/scripts/dogfood_savings.py +490 -0
- package/scripts/install-skills.ps1 +4 -334
- package/scripts/install-skills.sh +4 -435
- package/scripts/postinstall-interactive.js +0 -27
- package/scripts/postinstall.js +21 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +115 -0
- package/src/superlocalmemory/cli/commands.py +439 -41
- package/src/superlocalmemory/cli/main.py +92 -4
- package/src/superlocalmemory/cli/setup_wizard.py +47 -6
- package/src/superlocalmemory/core/backend_orchestrator.py +12 -8
- package/src/superlocalmemory/core/config.py +194 -9
- package/src/superlocalmemory/core/embeddings.py +10 -5
- package/src/superlocalmemory/core/engine.py +76 -5
- package/src/superlocalmemory/core/fact_consolidator.py +20 -3
- package/src/superlocalmemory/core/platform_utils.py +8 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/recall_worker.py +7 -0
- package/src/superlocalmemory/core/store_pipeline.py +23 -1
- package/src/superlocalmemory/core/worker_pool.py +14 -2
- package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
- package/src/superlocalmemory/hooks/portable_kit.py +506 -0
- package/src/superlocalmemory/hooks/session_registry.py +8 -4
- package/src/superlocalmemory/infra/cloud_backup.py +99 -23
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +15 -6
- package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
- package/src/superlocalmemory/mcp/server.py +75 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
- package/src/superlocalmemory/mcp/tools_core.py +37 -4
- package/src/superlocalmemory/mcp/tools_v3.py +6 -1
- package/src/superlocalmemory/mcp/tools_v33.py +8 -4
- package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
- package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
- package/src/superlocalmemory/optimize/cache/manager.py +92 -6
- package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
- package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
- package/src/superlocalmemory/optimize/compress/router.py +46 -13
- package/src/superlocalmemory/optimize/config/schema.py +6 -0
- package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
- package/src/superlocalmemory/optimize/proxy/server.py +11 -0
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
- package/src/superlocalmemory/optimize/storage/db.py +30 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +12 -2
- package/src/superlocalmemory/retrieval/engine.py +36 -3
- package/src/superlocalmemory/retrieval/entity_channel.py +5 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +10 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +10 -2
- package/src/superlocalmemory/server/recall_serializer.py +3 -1
- package/src/superlocalmemory/server/unified_daemon.py +156 -16
- package/src/superlocalmemory/storage/database.py +215 -43
- package/src/superlocalmemory/storage/migration_runner.py +17 -1
- package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +120 -0
- package/src/superlocalmemory/storage/models.py +10 -0
- package/src/superlocalmemory/storage/schema.py +15 -10
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
- package/src/superlocalmemory/ui/index.html +2 -2
- package/src/superlocalmemory/ui/js/core.js +98 -0
- package/src/superlocalmemory/ui/js/dashboard.js +8 -1
- package/src/superlocalmemory/ui/js/ide-status.js +16 -3
- package/src/superlocalmemory/ui/js/math-health.js +15 -3
- package/src/superlocalmemory/ui/js/optimize.js +18 -2
- package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +191 -741
- package/src/superlocalmemory.egg-info/SOURCES.txt +7 -9
- package/src/superlocalmemory.egg-info/requires.txt +1 -0
- package/ide/skills/slm-build-graph/SKILL.md +0 -423
- package/ide/skills/slm-list-recent/SKILL.md +0 -348
- package/ide/skills/slm-recall/SKILL.md +0 -326
- package/ide/skills/slm-remember/SKILL.md +0 -194
- package/ide/skills/slm-show-patterns/SKILL.md +0 -224
- package/ide/skills/slm-status/SKILL.md +0 -363
- package/ide/skills/slm-switch-profile/SKILL.md +0 -442
- package/skills/slm-build-graph/SKILL.md +0 -423
- package/skills/slm-list-recent/SKILL.md +0 -348
- package/skills/slm-optimize/README.md +0 -55
- package/skills/slm-optimize/SKILL.md +0 -139
- package/skills/slm-recall/SKILL.md +0 -343
- package/skills/slm-remember/SKILL.md +0 -194
- package/skills/slm-show-patterns/SKILL.md +0 -224
- package/skills/slm-status/SKILL.md +0 -363
- package/skills/slm-switch-profile/SKILL.md +0 -442
- package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
- package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
- 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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
93
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
122
|
-
store_path.chmod(0o600)
|
|
198
|
+
_atomic_write_creds(store_path, data)
|
|
123
199
|
deleted = True
|
|
124
|
-
except Exception:
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 = [
|