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.54",
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.4.54"
3
+ version = "3.4.55"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0-or-later"}
@@ -239,11 +239,63 @@ if [ -f "${REPO_DIR}/mcp_server.py" ]; then
239
239
  echo "✓ MCP server copied"
240
240
  fi
241
241
 
242
- # Copy config if not exists
243
- if [ ! -f "${INSTALL_DIR}/config.json" ]; then
244
- echo "Creating default config..."
245
- cp "${REPO_DIR}/config.json" "${INSTALL_DIR}/config.json"
246
- echo " Config created"
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
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
5
5
 
6
- __version__ = "3.4.54"
6
+ __version__ = "3.4.55"
7
7
 
8
8
  _REQUIRED_VERSIONS = {
9
9
  "sentence_transformers": "5.3.0",
@@ -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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superlocalmemory
3
- Version: 3.4.54
3
+ Version: 3.4.55
4
4
  Summary: Information-geometric agent memory with mathematical guarantees
5
5
  Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
6
6
  License: AGPL-3.0-or-later