superlocalmemory 3.4.22 → 3.4.24

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 (29) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/skills/slm-build-graph/SKILL.md +1 -1
  5. package/skills/slm-list-recent/SKILL.md +1 -1
  6. package/skills/slm-recall/SKILL.md +1 -1
  7. package/skills/slm-remember/SKILL.md +1 -1
  8. package/skills/slm-status/SKILL.md +1 -1
  9. package/skills/slm-switch-profile/SKILL.md +1 -1
  10. package/src/superlocalmemory/__init__.py +3 -0
  11. package/src/superlocalmemory/core/config.py +66 -18
  12. package/src/superlocalmemory/core/context_cache.py +1 -1
  13. package/src/superlocalmemory/core/embedding_worker.py +8 -27
  14. package/src/superlocalmemory/core/embeddings.py +83 -1
  15. package/src/superlocalmemory/core/engine_wiring.py +8 -0
  16. package/src/superlocalmemory/core/platform_utils.py +127 -0
  17. package/src/superlocalmemory/core/recall_worker.py +8 -24
  18. package/src/superlocalmemory/core/reranker_worker.py +8 -24
  19. package/src/superlocalmemory/core/worker_pool.py +2 -1
  20. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  21. package/src/superlocalmemory/learning/database.py +1 -1
  22. package/src/superlocalmemory/retrieval/reranker.py +2 -1
  23. package/src/superlocalmemory/server/routes/brain.py +1 -1
  24. package/src/superlocalmemory/server/routes/v3_api.py +150 -8
  25. package/src/superlocalmemory/server/security_middleware.py +20 -2
  26. package/src/superlocalmemory/server/unified_daemon.py +107 -5
  27. package/src/superlocalmemory/ui/index.html +50 -1
  28. package/src/superlocalmemory/ui/js/auto-settings.js +131 -5
  29. package/src/superlocalmemory/ui/js/core.js +96 -1
@@ -353,20 +353,28 @@ async function saveAllSettings() {
353
353
  if (statusEl) { statusEl.textContent = 'Saving...'; statusEl.style.display = 'inline'; statusEl.className = 'ms-2 text-muted'; }
354
354
 
355
355
  try {
356
- // Save mode
356
+ // V3.4.24: Include embedding params in save payload
357
+ var embParams = getEmbeddingParams();
358
+ var payload = Object.assign({mode: mode, provider: provider, model: model, api_key: apiKey}, embParams);
357
359
  var modeResp = await fetch('/api/v3/mode/set', {
358
360
  method: 'POST',
359
361
  headers: {'Content-Type': 'application/json'},
360
- body: JSON.stringify({mode: mode, provider: provider, model: model, api_key: apiKey})
362
+ body: JSON.stringify(payload)
361
363
  });
362
364
 
363
365
  if (modeResp.ok) {
366
+ var modeData = await modeResp.json();
367
+ var msg = 'Configuration saved! Mode: ' + mode.toUpperCase() +
368
+ (provider !== 'none' ? ' | Provider: ' + provider : '');
369
+ if (modeData.needs_reindex) {
370
+ msg += ' | Embeddings will be re-indexed on next use (may take several minutes).';
371
+ }
364
372
  if (statusEl) {
365
- statusEl.textContent = 'Configuration saved! Mode: ' + mode.toUpperCase() +
366
- (provider !== 'none' ? ' | Provider: ' + provider : '');
367
- statusEl.className = 'ms-2 text-success fw-bold';
373
+ statusEl.textContent = msg;
374
+ statusEl.className = modeData.needs_reindex ? 'ms-2 text-warning fw-bold' : 'ms-2 text-success fw-bold';
368
375
  }
369
376
  loadModeSettings();
377
+ loadEmbeddingSettings();
370
378
  } else {
371
379
  if (statusEl) { statusEl.textContent = 'Save failed'; statusEl.className = 'ms-2 text-danger'; }
372
380
  }
@@ -381,10 +389,127 @@ async function saveAllSettings() {
381
389
  }, 5000);
382
390
  }
383
391
 
392
+ // ============================================================================
393
+ // Embedding Configuration (V3.4.24 — Custom OpenAI-compatible endpoints)
394
+ // ============================================================================
395
+
396
+ async function loadEmbeddingSettings() {
397
+ try {
398
+ var resp = await fetch('/api/v3/embedding/config');
399
+ if (!resp.ok) return;
400
+ var data = await resp.json();
401
+
402
+ var provEl = document.getElementById('settings-emb-provider');
403
+ if (provEl) {
404
+ provEl.value = data.is_openai_compatible ? 'openai' : 'default';
405
+ }
406
+
407
+ if (data.is_openai_compatible) {
408
+ var modelEl = document.getElementById('settings-emb-model');
409
+ if (modelEl) modelEl.value = data.model_name || '';
410
+ var dimEl = document.getElementById('settings-emb-dimension');
411
+ if (dimEl) dimEl.value = data.dimension || '';
412
+ var epEl = document.getElementById('settings-emb-endpoint');
413
+ if (epEl) epEl.value = data.api_endpoint || '';
414
+ }
415
+
416
+ updateEmbeddingUI();
417
+
418
+ var info = document.getElementById('settings-emb-info');
419
+ if (info) {
420
+ var _name = (data.model_name || 'unknown').replace(/[<>&"']/g, function(c) {
421
+ return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c];
422
+ });
423
+ if (data.is_openai_compatible) {
424
+ info.innerHTML = 'Using custom endpoint: <strong>' + _name + '</strong> (' + data.dimension + 'd)';
425
+ } else {
426
+ info.innerHTML = 'Using local <strong>' + _name + '</strong> (' + data.dimension + 'd)';
427
+ }
428
+ }
429
+ } catch (e) {
430
+ console.log('Load embedding settings error:', e);
431
+ }
432
+ }
433
+
434
+ function updateEmbeddingUI() {
435
+ var provider = document.getElementById('settings-emb-provider')?.value || 'default';
436
+ var isCustom = provider === 'openai';
437
+
438
+ var modelCol = document.getElementById('settings-emb-model-col');
439
+ var dimCol = document.getElementById('settings-emb-dim-col');
440
+ var endpointRow = document.getElementById('settings-emb-endpoint-row');
441
+ var testRow = document.getElementById('settings-emb-test-row');
442
+
443
+ if (modelCol) modelCol.style.display = isCustom ? 'block' : 'none';
444
+ if (dimCol) dimCol.style.display = isCustom ? 'block' : 'none';
445
+ if (endpointRow) endpointRow.style.display = isCustom ? 'flex' : 'none';
446
+ if (testRow) testRow.style.display = isCustom ? 'block' : 'none';
447
+
448
+ var info = document.getElementById('settings-emb-info');
449
+ if (info && !isCustom) {
450
+ info.innerHTML = 'Using local <strong>nomic-embed-text-v1.5</strong> (768d)';
451
+ }
452
+ }
453
+
454
+ async function testEmbeddingEndpoint() {
455
+ var endpoint = document.getElementById('settings-emb-endpoint')?.value || '';
456
+ var model = document.getElementById('settings-emb-model')?.value || '';
457
+ var key = document.getElementById('settings-emb-key')?.value || '';
458
+ var resultEl = document.getElementById('settings-emb-test-result');
459
+
460
+ if (!endpoint) {
461
+ if (resultEl) { resultEl.textContent = 'Enter an endpoint first'; resultEl.className = 'ms-2 small text-danger'; }
462
+ return;
463
+ }
464
+
465
+ if (resultEl) { resultEl.textContent = 'Testing...'; resultEl.className = 'ms-2 small text-muted'; }
466
+
467
+ try {
468
+ var resp = await fetch('/api/v3/embedding/test', {
469
+ method: 'POST',
470
+ headers: {'Content-Type': 'application/json'},
471
+ body: JSON.stringify({api_endpoint: endpoint, model_name: model, api_key: key})
472
+ });
473
+ var data = await resp.json();
474
+ if (data.success) {
475
+ if (resultEl) { resultEl.textContent = data.message; resultEl.className = 'ms-2 small text-success fw-bold'; }
476
+ var dimEl = document.getElementById('settings-emb-dimension');
477
+ if (dimEl && data.dimension) {
478
+ if (!dimEl.value) {
479
+ dimEl.value = data.dimension;
480
+ } else if (parseInt(dimEl.value) !== data.dimension) {
481
+ if (resultEl) {
482
+ resultEl.textContent = 'Connected! Warning: endpoint returns ' + data.dimension + 'd but you entered ' + dimEl.value + 'd';
483
+ resultEl.className = 'ms-2 small text-warning fw-bold';
484
+ }
485
+ }
486
+ }
487
+ } else {
488
+ if (resultEl) { resultEl.textContent = 'Failed: ' + (data.error || 'Unknown'); resultEl.className = 'ms-2 small text-danger'; }
489
+ }
490
+ } catch (e) {
491
+ if (resultEl) { resultEl.textContent = 'Error: ' + e.message; resultEl.className = 'ms-2 small text-danger'; }
492
+ }
493
+ }
494
+
495
+ function getEmbeddingParams() {
496
+ var provider = document.getElementById('settings-emb-provider')?.value || 'default';
497
+ if (provider !== 'openai') return {};
498
+ return {
499
+ embedding_provider: 'openai',
500
+ embedding_endpoint: document.getElementById('settings-emb-endpoint')?.value || '',
501
+ embedding_model: document.getElementById('settings-emb-model')?.value || '',
502
+ embedding_dimension: parseInt(document.getElementById('settings-emb-dimension')?.value) || 0,
503
+ embedding_key: document.getElementById('settings-emb-key')?.value || '',
504
+ };
505
+ }
506
+
384
507
  // Bind events
385
508
  document.getElementById('settings-provider')?.addEventListener('change', updateProviderUI);
386
509
  document.getElementById('settings-save-all')?.addEventListener('click', saveAllSettings);
387
510
  document.getElementById('settings-test-btn')?.addEventListener('click', testConnection);
511
+ document.getElementById('settings-emb-provider')?.addEventListener('change', updateEmbeddingUI);
512
+ document.getElementById('settings-emb-test-btn')?.addEventListener('click', testEmbeddingEndpoint);
388
513
 
389
514
  // Mode radio buttons
390
515
  document.querySelectorAll('input[name="settings-mode-radio"]').forEach(function(radio) {
@@ -395,5 +520,6 @@ document.querySelectorAll('input[name="settings-mode-radio"]').forEach(function(
395
520
  document.getElementById('settings-tab')?.addEventListener('shown.bs.tab', function() {
396
521
  loadAutoSettings();
397
522
  loadModeSettings();
523
+ loadEmbeddingSettings();
398
524
  updateModeUI();
399
525
  });
@@ -3,6 +3,97 @@
3
3
  // Security: All dynamic text MUST pass through escapeHtml() before DOM insertion.
4
4
  // Data originates from our own trusted local SQLite database (localhost only).
5
5
 
6
+ // ============================================================================
7
+ // v3.4.23 — slmFetch(): fetch with 15s abort timeout
8
+ // ----------------------------------------------------------------------------
9
+ // Bare fetch() never resolves when the daemon dies mid-request (socket kept
10
+ // open, Promise pending). That leaves dashboard spinners running forever and
11
+ // stacks up orphan fetches that make hard-refresh hang. slmFetch wraps every
12
+ // request in an AbortController with a 15 s ceiling, so a dead daemon
13
+ // surfaces as a normal rejection and the UI can show a clear error.
14
+ // ============================================================================
15
+
16
+ window.SLM_FETCH_TIMEOUT_MS = 15000;
17
+
18
+ // Global fetch patch: apply the abort timeout to every relative-URL request
19
+ // automatically. 17 UI modules call bare fetch() — patching here avoids
20
+ // touching each one and guarantees no future callsite can regress to an
21
+ // un-timed fetch that holds the spinner forever. Absolute URLs (external
22
+ // resources) are passed through unchanged. Callers that already supply
23
+ // `signal` keep their own behavior. `init.timeoutMs` lets callers override
24
+ // the default per-request.
25
+ (function patchFetch() {
26
+ if (window.__slmFetchPatched) return;
27
+ window.__slmFetchPatched = true;
28
+ var _origFetch = window.fetch.bind(window);
29
+ window.fetch = function (input, init) {
30
+ init = init || {};
31
+ var urlStr = typeof input === 'string' ? input : (input && input.url) || '';
32
+ var isRelative = !(/^https?:\/\//i.test(urlStr));
33
+ if (!isRelative || init.signal) {
34
+ return _origFetch(input, init);
35
+ }
36
+ var controller = new AbortController();
37
+ var timeoutMs = init.timeoutMs || window.SLM_FETCH_TIMEOUT_MS;
38
+ var timer = setTimeout(function () { controller.abort(); }, timeoutMs);
39
+ init.signal = controller.signal;
40
+ return _origFetch(input, init).finally(function () { clearTimeout(timer); });
41
+ };
42
+ })();
43
+
44
+ // Thin named wrapper for callsites that want explicit timeout control.
45
+ // Equivalent to the patched fetch above but accepts `init.timeoutMs`.
46
+ async function slmFetch(input, init) {
47
+ return fetch(input, init || {});
48
+ }
49
+
50
+ // ============================================================================
51
+ // v3.4.23 — version fingerprint + auto-reload on daemon upgrade
52
+ // ----------------------------------------------------------------------------
53
+ // index.html ships with <meta name="slm-version" content="__SLM_VERSION__">
54
+ // that the server fills in at serve time. After page load we ask the daemon
55
+ // for its current version via /api/version; on mismatch we clear localStorage
56
+ // and hard-reload once, so a stale tab never lingers after `slm restart` or
57
+ // a package upgrade. Guarded by sessionStorage to avoid reload loops.
58
+ // ============================================================================
59
+
60
+ async function checkVersionFingerprint() {
61
+ try {
62
+ var metaEl = document.querySelector('meta[name="slm-version"]');
63
+ var pageVersion = metaEl ? metaEl.getAttribute('content') : null;
64
+ if (!pageVersion || pageVersion === '__SLM_VERSION__') return;
65
+ var resp = await slmFetch('/api/version', { timeoutMs: 5000 });
66
+ if (!resp.ok) return;
67
+ var data = await resp.json();
68
+ var serverVersion = data && data.version;
69
+ if (!serverVersion || serverVersion === pageVersion) return;
70
+ try {
71
+ if (sessionStorage.getItem('slm-version-reload-done') === serverVersion) {
72
+ console.warn('[slm] version mismatch persists after reload:',
73
+ pageVersion, '!=', serverVersion);
74
+ return;
75
+ }
76
+ sessionStorage.setItem('slm-version-reload-done', serverVersion);
77
+ } catch (e) {
78
+ // sessionStorage blocked (private mode, quota, etc.) — fall through
79
+ // to reload. Worst case: we reload twice instead of once, still
80
+ // safe because server version converges on second attempt.
81
+ }
82
+ try {
83
+ // Preserve theme; drop everything else that might be stale.
84
+ var theme = localStorage.getItem('slm-theme');
85
+ localStorage.clear();
86
+ if (theme) localStorage.setItem('slm-theme', theme);
87
+ } catch (e) { /* localStorage may be blocked */ }
88
+ console.info('[slm] daemon upgraded', pageVersion, '->', serverVersion,
89
+ '— reloading');
90
+ location.reload();
91
+ } catch (err) {
92
+ // Network error or daemon down: don't reload, just log.
93
+ console.debug('[slm] version check skipped:', err && err.message);
94
+ }
95
+ }
96
+
6
97
  // ============================================================================
7
98
  // Dark Mode
8
99
  // ============================================================================
@@ -180,7 +271,7 @@ function formatDateFull(dateString) {
180
271
 
181
272
  async function loadStats() {
182
273
  try {
183
- var response = await fetch('/api/stats');
274
+ var response = await slmFetch('/api/stats');
184
275
  var data = await response.json();
185
276
  var ov = data.overview || {};
186
277
  animateCounter('stat-memories', ov.total_memories || 0);
@@ -235,6 +326,10 @@ function populateFilters(categories, projects) {
235
326
 
236
327
  window.addEventListener('DOMContentLoaded', function() {
237
328
  initDarkMode();
329
+ // v3.4.23: version check runs first and non-blocking. If a mismatch is
330
+ // detected it triggers location.reload(), so the rest of init on the
331
+ // stale page becomes a no-op.
332
+ checkVersionFingerprint();
238
333
  loadProfiles();
239
334
  loadStats();
240
335
  loadGraph();