superlocalmemory 3.4.54 → 3.4.55
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.55",
|
|
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
package/scripts/install.sh
CHANGED
|
@@ -239,11 +239,63 @@ if [ -f "${REPO_DIR}/mcp_server.py" ]; then
|
|
|
239
239
|
echo "✓ MCP server copied"
|
|
240
240
|
fi
|
|
241
241
|
|
|
242
|
-
#
|
|
243
|
-
if [ ! -f "${INSTALL_DIR}/
|
|
244
|
-
echo "
|
|
245
|
-
|
|
246
|
-
echo "
|
|
242
|
+
# Interactive mode selection (v3.4.55)
|
|
243
|
+
if [ ! -f "${INSTALL_DIR}/current_mode" ] && [ "${NON_INTERACTIVE}" != "true" ]; then
|
|
244
|
+
echo ""
|
|
245
|
+
echo "┌─────────────────────────────────────────────┐"
|
|
246
|
+
echo "│ SuperLocalMemory V3 — Setup │"
|
|
247
|
+
echo "└─────────────────────────────────────────────┘"
|
|
248
|
+
echo ""
|
|
249
|
+
echo " Choose operating mode:"
|
|
250
|
+
echo ""
|
|
251
|
+
echo " [A] Zero-Cloud — Pure local, no API keys needed"
|
|
252
|
+
echo " • Embedding: sentence-transformers (local)"
|
|
253
|
+
echo " • LLM: none"
|
|
254
|
+
echo " • Best for: privacy, air-gapped, EU AI Act"
|
|
255
|
+
echo ""
|
|
256
|
+
echo " [B] Local AI — Ollama-powered (recommended)"
|
|
257
|
+
echo " • Embedding: ollama / nomic-embed-text"
|
|
258
|
+
echo " • LLM: ollama / llama3.2"
|
|
259
|
+
echo " • Best for: full offline AI, zero cost"
|
|
260
|
+
echo ""
|
|
261
|
+
echo " [C] Cloud Power — OpenRouter / OpenAI API"
|
|
262
|
+
echo " • Embedding: text-embedding-3-large"
|
|
263
|
+
echo " • LLM: claude-sonnet-4 / gpt-4.1-mini"
|
|
264
|
+
echo " • Best for: max quality, API required"
|
|
265
|
+
echo ""
|
|
266
|
+
read -r -p " Enter mode [A/B/C] (default: B): " MODE_CHOICE
|
|
267
|
+
MODE_CHOICE=${MODE_CHOICE:-b}
|
|
268
|
+
case "${MODE_CHOICE,,}" in
|
|
269
|
+
a) SELECTED_MODE="a" ;;
|
|
270
|
+
c) SELECTED_MODE="c" ;;
|
|
271
|
+
*) SELECTED_MODE="b" ;;
|
|
272
|
+
esac
|
|
273
|
+
echo ""
|
|
274
|
+
echo " → Selected Mode ${SELECTED_MODE^^}"
|
|
275
|
+
echo ""
|
|
276
|
+
# Generate initial 3-mode config via Python
|
|
277
|
+
python3 -c "
|
|
278
|
+
from superlocalmemory.core.config import SLMConfig
|
|
279
|
+
SLMConfig.migrate_to_3mode()
|
|
280
|
+
SLMConfig.switch_mode('${SELECTED_MODE}')
|
|
281
|
+
print('✓ Mode ${SELECTED_MODE^^} configured')
|
|
282
|
+
print(' Config files created: mode_a.json, mode_b.json, mode_c.json')
|
|
283
|
+
print(' Run slm mode a/b/c to switch modes later')
|
|
284
|
+
"
|
|
285
|
+
elif [ ! -f "${INSTALL_DIR}/config.json" ] && [ "${NON_INTERACTIVE}" = "true" ]; then
|
|
286
|
+
echo "Creating default config (non-interactive, Mode B)..."
|
|
287
|
+
python3 -c "
|
|
288
|
+
from superlocalmemory.core.config import SLMConfig
|
|
289
|
+
SLMConfig.migrate_to_3mode()
|
|
290
|
+
print('✓ Default config created (Mode B)')
|
|
291
|
+
"
|
|
292
|
+
elif [ -f "${INSTALL_DIR}/config.json" ] && [ ! -f "${INSTALL_DIR}/current_mode" ]; then
|
|
293
|
+
echo "Migrating existing config to 3-mode system..."
|
|
294
|
+
python3 -c "
|
|
295
|
+
from superlocalmemory.core.config import SLMConfig
|
|
296
|
+
SLMConfig.migrate_to_3mode()
|
|
297
|
+
print('✓ Config migrated — your settings preserved')
|
|
298
|
+
"
|
|
247
299
|
else
|
|
248
300
|
echo "○ Config exists (keeping existing)"
|
|
249
301
|
fi
|
|
@@ -1098,6 +1098,192 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
1098
1098
|
async def api_version():
|
|
1099
1099
|
return JSONResponse({"version": _SLM_VERSION})
|
|
1100
1100
|
|
|
1101
|
+
# v3.4.55: Mode switching & config API for the dashboard UI.
|
|
1102
|
+
# The auto-settings.js expects /api/v3/* endpoints. These routes
|
|
1103
|
+
# bridge the 3-mode config system to the existing settings page.
|
|
1104
|
+
|
|
1105
|
+
@application.get("/api/v3/auto")
|
|
1106
|
+
async def v3_auto_detect():
|
|
1107
|
+
"""Auto-detect available providers from environment."""
|
|
1108
|
+
import os as _os
|
|
1109
|
+
providers = []
|
|
1110
|
+
if _os.environ.get("OPENROUTER_API_KEY"):
|
|
1111
|
+
providers.append({"id": "openrouter", "name": "OpenRouter", "has_key": True})
|
|
1112
|
+
if _os.environ.get("OPENAI_API_KEY"):
|
|
1113
|
+
providers.append({"id": "openai", "name": "OpenAI", "has_key": True})
|
|
1114
|
+
if _os.environ.get("ANTHROPIC_API_KEY"):
|
|
1115
|
+
providers.append({"id": "anthropic", "name": "Anthropic", "has_key": True})
|
|
1116
|
+
# Ollama is always available as a local option if the server is reachable
|
|
1117
|
+
try:
|
|
1118
|
+
import httpx as _hx
|
|
1119
|
+
_r = _hx.get("http://localhost:11434/api/tags", timeout=2.0)
|
|
1120
|
+
ollama_models = []
|
|
1121
|
+
if _r.status_code == 200:
|
|
1122
|
+
ollama_models = [m["name"] for m in _r.json().get("models", [])]
|
|
1123
|
+
providers.append({
|
|
1124
|
+
"id": "ollama", "name": "Ollama (local)",
|
|
1125
|
+
"has_key": False, "running": True,
|
|
1126
|
+
"models": ollama_models,
|
|
1127
|
+
})
|
|
1128
|
+
except Exception:
|
|
1129
|
+
providers.append({
|
|
1130
|
+
"id": "ollama", "name": "Ollama (local)",
|
|
1131
|
+
"has_key": False, "running": False, "models": [],
|
|
1132
|
+
})
|
|
1133
|
+
return {"providers": providers}
|
|
1134
|
+
|
|
1135
|
+
@application.get("/api/v3/mode")
|
|
1136
|
+
async def v3_get_mode():
|
|
1137
|
+
"""Get current mode and available modes."""
|
|
1138
|
+
from superlocalmemory.core.config import SLMConfig
|
|
1139
|
+
from superlocalmemory.storage.models import Mode as _M
|
|
1140
|
+
_base = Path.home() / ".superlocalmemory"
|
|
1141
|
+
current = SLMConfig.read_current_mode(_base)
|
|
1142
|
+
modes = {}
|
|
1143
|
+
for _m in (_M.A, _M.B, _M.C):
|
|
1144
|
+
_name = _m.value.lower()
|
|
1145
|
+
_path = SLMConfig._mode_config_path(_base, _m)
|
|
1146
|
+
_cfg = None
|
|
1147
|
+
if _path.exists():
|
|
1148
|
+
try:
|
|
1149
|
+
_cfg = SLMConfig.load(_path)
|
|
1150
|
+
except Exception:
|
|
1151
|
+
pass
|
|
1152
|
+
modes[_name] = {
|
|
1153
|
+
"label": {"a": "Zero-Cloud", "b": "Local AI", "c": "Cloud Power"}[_name],
|
|
1154
|
+
"config_exists": _path.exists(),
|
|
1155
|
+
"embedding_provider": getattr(_cfg.embedding, "provider", "") if _cfg else "",
|
|
1156
|
+
"embedding_model": getattr(_cfg.embedding, "model_name", "") if _cfg else "",
|
|
1157
|
+
"llm_provider": getattr(_cfg.llm, "provider", "") if _cfg else "",
|
|
1158
|
+
"llm_model": getattr(_cfg.llm, "model", "") if _cfg else "",
|
|
1159
|
+
"reranker": _cfg.retrieval.use_cross_encoder if _cfg else True,
|
|
1160
|
+
}
|
|
1161
|
+
return {"current_mode": current, "modes": modes}
|
|
1162
|
+
|
|
1163
|
+
@application.post("/api/v3/mode/set")
|
|
1164
|
+
async def v3_set_mode(request: Request):
|
|
1165
|
+
"""Switch mode and optionally update provider/model. Body matches
|
|
1166
|
+
the auto-settings.js saveSettings() payload."""
|
|
1167
|
+
from superlocalmemory.core.config import SLMConfig
|
|
1168
|
+
try:
|
|
1169
|
+
body = await request.json()
|
|
1170
|
+
new_mode = (body.get("mode") or body.get("settings_mode") or "").lower().strip()
|
|
1171
|
+
if new_mode not in ("a", "b", "c"):
|
|
1172
|
+
return JSONResponse(
|
|
1173
|
+
{"ok": False, "error": "mode must be a, b, or c"},
|
|
1174
|
+
status_code=400,
|
|
1175
|
+
)
|
|
1176
|
+
config = SLMConfig.switch_mode(new_mode)
|
|
1177
|
+
|
|
1178
|
+
# If provider/model were sent, update the saved config
|
|
1179
|
+
provider = body.get("provider", "").strip()
|
|
1180
|
+
if provider and new_mode != "a":
|
|
1181
|
+
_base = Path.home() / ".superlocalmemory"
|
|
1182
|
+
from superlocalmemory.core.config import LLMConfig, EmbeddingConfig
|
|
1183
|
+
# Update LLM
|
|
1184
|
+
model = body.get("model", "").strip()
|
|
1185
|
+
api_key = body.get("api_key", "").strip()
|
|
1186
|
+
endpoint = body.get("endpoint", "").strip()
|
|
1187
|
+
if provider or model:
|
|
1188
|
+
config.llm = LLMConfig(
|
|
1189
|
+
provider=provider or config.llm.provider,
|
|
1190
|
+
model=model or config.llm.model,
|
|
1191
|
+
api_key=api_key or config.llm.api_key,
|
|
1192
|
+
api_base=endpoint or config.llm.api_base,
|
|
1193
|
+
)
|
|
1194
|
+
# Update embedding
|
|
1195
|
+
emb_provider = body.get("embedding_provider", "").strip()
|
|
1196
|
+
emb_model = body.get("embedding_model", "").strip()
|
|
1197
|
+
emb_key = body.get("embedding_key", "").strip()
|
|
1198
|
+
if emb_provider or emb_model:
|
|
1199
|
+
config.embedding = EmbeddingConfig(
|
|
1200
|
+
provider=emb_provider or config.embedding.provider,
|
|
1201
|
+
model_name=emb_model or config.embedding.model_name,
|
|
1202
|
+
dimension=config.embedding.dimension,
|
|
1203
|
+
api_key=emb_key or config.embedding.api_key,
|
|
1204
|
+
)
|
|
1205
|
+
config.save(mode_change=True)
|
|
1206
|
+
|
|
1207
|
+
return {
|
|
1208
|
+
"ok": True, "mode": new_mode,
|
|
1209
|
+
"embedding": f"{config.embedding.provider}/{config.embedding.model_name}",
|
|
1210
|
+
"llm": f"{config.llm.provider}/{config.llm.model}",
|
|
1211
|
+
"message": f"Switched to Mode {new_mode.upper()}. Run slm restart to apply.",
|
|
1212
|
+
}
|
|
1213
|
+
except Exception as exc:
|
|
1214
|
+
return JSONResponse({"ok": False, "error": str(exc)}, status_code=500)
|
|
1215
|
+
|
|
1216
|
+
@application.get("/api/v3/ollama/status")
|
|
1217
|
+
async def v3_ollama_status():
|
|
1218
|
+
"""Check if Ollama is running and list available models."""
|
|
1219
|
+
try:
|
|
1220
|
+
import httpx as _hx
|
|
1221
|
+
_r = _hx.get("http://localhost:11434/api/tags", timeout=3.0)
|
|
1222
|
+
if _r.status_code == 200:
|
|
1223
|
+
_data = _r.json()
|
|
1224
|
+
return {
|
|
1225
|
+
"running": True,
|
|
1226
|
+
"models": [{"name": m["name"], "size": m.get("size", 0)}
|
|
1227
|
+
for m in _data.get("models", [])],
|
|
1228
|
+
}
|
|
1229
|
+
except Exception:
|
|
1230
|
+
pass
|
|
1231
|
+
return {"running": False, "models": []}
|
|
1232
|
+
|
|
1233
|
+
@application.post("/api/v3/provider/test")
|
|
1234
|
+
async def v3_provider_test(request: Request):
|
|
1235
|
+
"""Test a provider connection. Body: {provider, api_key, endpoint}."""
|
|
1236
|
+
try:
|
|
1237
|
+
body = await request.json()
|
|
1238
|
+
provider = body.get("provider", "")
|
|
1239
|
+
api_key = body.get("api_key", "")
|
|
1240
|
+
endpoint = body.get("endpoint", "")
|
|
1241
|
+
if provider == "ollama":
|
|
1242
|
+
import httpx as _hx
|
|
1243
|
+
_r = _hx.get(f"{endpoint or 'http://localhost:11434'}/api/tags", timeout=3.0)
|
|
1244
|
+
return {"ok": _r.status_code == 200, "message": "Ollama reachable" if _r.status_code == 200 else f"HTTP {_r.status_code}"}
|
|
1245
|
+
if provider in ("openai", "openrouter"):
|
|
1246
|
+
import httpx as _hx
|
|
1247
|
+
_url = f"{endpoint or 'https://api.openai.com/v1'}/models"
|
|
1248
|
+
_headers = {"Authorization": f"Bearer {api_key}"}
|
|
1249
|
+
_r = _hx.get(_url, headers=_headers, timeout=5.0)
|
|
1250
|
+
return {"ok": _r.status_code == 200, "message": "API key valid" if _r.status_code == 200 else f"HTTP {_r.status_code}: {_r.text[:200]}"}
|
|
1251
|
+
return {"ok": False, "message": f"Unknown provider: {provider}"}
|
|
1252
|
+
except Exception as exc:
|
|
1253
|
+
return {"ok": False, "message": str(exc)}
|
|
1254
|
+
|
|
1255
|
+
@application.get("/api/v3/embedding/config")
|
|
1256
|
+
async def v3_get_embedding_config():
|
|
1257
|
+
"""Get current embedding configuration."""
|
|
1258
|
+
engine = getattr(application.state, "engine", None)
|
|
1259
|
+
if engine is None:
|
|
1260
|
+
return JSONResponse({"ok": False, "error": "engine not initialized"}, status_code=503)
|
|
1261
|
+
config = getattr(engine, "_config", None)
|
|
1262
|
+
if config is None:
|
|
1263
|
+
return JSONResponse({"ok": False, "error": "no config loaded"}, status_code=503)
|
|
1264
|
+
return {
|
|
1265
|
+
"provider": getattr(config.embedding, "provider", ""),
|
|
1266
|
+
"model_name": getattr(config.embedding, "model_name", ""),
|
|
1267
|
+
"dimension": getattr(config.embedding, "dimension", 0),
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
@application.post("/api/v3/embedding/test")
|
|
1271
|
+
async def v3_embedding_test(request: Request):
|
|
1272
|
+
"""Test embedding with current config. Body: {text: \"test\"}."""
|
|
1273
|
+
try:
|
|
1274
|
+
body = await request.json()
|
|
1275
|
+
text = body.get("text", "test embedding")
|
|
1276
|
+
engine = getattr(application.state, "engine", None)
|
|
1277
|
+
if engine is None:
|
|
1278
|
+
return {"ok": False, "error": "engine not initialized"}
|
|
1279
|
+
embedder = getattr(engine, "_embedder", None)
|
|
1280
|
+
if embedder is None:
|
|
1281
|
+
return {"ok": False, "error": "embedder not available"}
|
|
1282
|
+
vec = embedder.embed(text)
|
|
1283
|
+
return {"ok": True, "dimensions": len(vec) if vec else 0}
|
|
1284
|
+
except Exception as exc:
|
|
1285
|
+
return {"ok": False, "error": str(exc)}
|
|
1286
|
+
|
|
1101
1287
|
@application.get("/", response_class=HTMLResponse)
|
|
1102
1288
|
async def root():
|
|
1103
1289
|
index_path = UI_DIR / "index.html"
|