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.
- package/CHANGELOG.md +29 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/slm-build-graph/SKILL.md +1 -1
- package/skills/slm-list-recent/SKILL.md +1 -1
- package/skills/slm-recall/SKILL.md +1 -1
- package/skills/slm-remember/SKILL.md +1 -1
- package/skills/slm-status/SKILL.md +1 -1
- package/skills/slm-switch-profile/SKILL.md +1 -1
- package/src/superlocalmemory/__init__.py +3 -0
- package/src/superlocalmemory/core/config.py +66 -18
- package/src/superlocalmemory/core/context_cache.py +1 -1
- package/src/superlocalmemory/core/embedding_worker.py +8 -27
- package/src/superlocalmemory/core/embeddings.py +83 -1
- package/src/superlocalmemory/core/engine_wiring.py +8 -0
- package/src/superlocalmemory/core/platform_utils.py +127 -0
- package/src/superlocalmemory/core/recall_worker.py +8 -24
- package/src/superlocalmemory/core/reranker_worker.py +8 -24
- package/src/superlocalmemory/core/worker_pool.py +2 -1
- package/src/superlocalmemory/hooks/context_payload.py +1 -1
- package/src/superlocalmemory/learning/database.py +1 -1
- package/src/superlocalmemory/retrieval/reranker.py +2 -1
- package/src/superlocalmemory/server/routes/brain.py +1 -1
- package/src/superlocalmemory/server/routes/v3_api.py +150 -8
- package/src/superlocalmemory/server/security_middleware.py +20 -2
- package/src/superlocalmemory/server/unified_daemon.py +107 -5
- package/src/superlocalmemory/ui/index.html +50 -1
- package/src/superlocalmemory/ui/js/auto-settings.js +131 -5
- 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
|
-
//
|
|
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(
|
|
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 =
|
|
366
|
-
|
|
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 {'<':'<','>':'>','&':'&','"':'"',"'":'''}[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
|
|
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();
|