superlocalmemory 3.8.14 → 4.0.1

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 (211) hide show
  1. package/ATTRIBUTION.md +4 -4
  2. package/CHANGELOG.md +124 -137
  3. package/README.md +69 -66
  4. package/docs/pi-dev-integration.md +1 -1
  5. package/package.json +6 -1
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +1 -1
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +1 -1
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +1 -1
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  29. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  31. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  32. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  33. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  36. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  38. package/pyproject.toml +11 -4
  39. package/src/superlocalmemory/__init__.py +1 -1
  40. package/src/superlocalmemory/cli/commands.py +125 -11
  41. package/src/superlocalmemory/cli/daemon.py +5 -1
  42. package/src/superlocalmemory/cli/main.py +35 -2
  43. package/src/superlocalmemory/cli/ops_cmd.py +281 -0
  44. package/src/superlocalmemory/cli/setup_wizard.py +1 -1
  45. package/src/superlocalmemory/compliance/audit.py +65 -0
  46. package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
  47. package/src/superlocalmemory/compliance/gdpr.py +416 -20
  48. package/src/superlocalmemory/compliance/retention.py +74 -22
  49. package/src/superlocalmemory/compliance/scheduler.py +78 -9
  50. package/src/superlocalmemory/core/actor_context.py +166 -0
  51. package/src/superlocalmemory/core/admission.py +549 -0
  52. package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
  53. package/src/superlocalmemory/core/config.py +202 -24
  54. package/src/superlocalmemory/core/consolidation_engine.py +13 -13
  55. package/src/superlocalmemory/core/context_cache.py +28 -0
  56. package/src/superlocalmemory/core/embeddings.py +64 -2
  57. package/src/superlocalmemory/core/engine.py +7 -2
  58. package/src/superlocalmemory/core/engine_ingestion.py +65 -3
  59. package/src/superlocalmemory/core/engine_wiring.py +32 -5
  60. package/src/superlocalmemory/core/ingest_policy.py +38 -0
  61. package/src/superlocalmemory/core/maintenance.py +255 -0
  62. package/src/superlocalmemory/core/modes.py +40 -13
  63. package/src/superlocalmemory/core/mutations.py +437 -44
  64. package/src/superlocalmemory/core/operation_policy.py +92 -0
  65. package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
  66. package/src/superlocalmemory/core/operation_request.py +127 -0
  67. package/src/superlocalmemory/core/ops_remediation.py +542 -0
  68. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  69. package/src/superlocalmemory/core/remember_runtime.py +202 -4
  70. package/src/superlocalmemory/core/remote_mode.py +20 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +150 -0
  72. package/src/superlocalmemory/core/topic_signature.py +19 -4
  73. package/src/superlocalmemory/core/transactions/__init__.py +78 -0
  74. package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
  75. package/src/superlocalmemory/core/transactions/erasure.py +825 -0
  76. package/src/superlocalmemory/core/transactions/manifest.py +255 -0
  77. package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
  78. package/src/superlocalmemory/core/transactions/obligations.py +272 -0
  79. package/src/superlocalmemory/core/transactions/owners.py +114 -0
  80. package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
  81. package/src/superlocalmemory/core/transactions/service.py +330 -0
  82. package/src/superlocalmemory/core/worker_pool.py +33 -5
  83. package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
  84. package/src/superlocalmemory/encoding/emotional.py +75 -14
  85. package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
  86. package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
  87. package/src/superlocalmemory/evolution/evolution_store.py +244 -4
  88. package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
  89. package/src/superlocalmemory/evolution/model_selection.py +18 -3
  90. package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
  91. package/src/superlocalmemory/evolution/skill_activator.py +270 -0
  92. package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
  93. package/src/superlocalmemory/evolution/types.py +30 -8
  94. package/src/superlocalmemory/graph/cozo_backend.py +17 -9
  95. package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
  96. package/src/superlocalmemory/hooks/auto_recall.py +64 -30
  97. package/src/superlocalmemory/hooks/codex_assets.py +14 -1
  98. package/src/superlocalmemory/infra/backup.py +434 -7
  99. package/src/superlocalmemory/infra/process_reaper.py +18 -0
  100. package/src/superlocalmemory/infra/self_heal.py +401 -0
  101. package/src/superlocalmemory/learning/feedback.py +52 -9
  102. package/src/superlocalmemory/loops/engine.py +10 -0
  103. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  104. package/src/superlocalmemory/mcp/http_transport.py +30 -331
  105. package/src/superlocalmemory/mcp/profiles.py +5 -0
  106. package/src/superlocalmemory/mcp/resources.py +8 -0
  107. package/src/superlocalmemory/mcp/server.py +51 -4
  108. package/src/superlocalmemory/mcp/shared.py +19 -0
  109. package/src/superlocalmemory/mcp/tools_active.py +25 -4
  110. package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
  111. package/src/superlocalmemory/mcp/tools_context.py +50 -8
  112. package/src/superlocalmemory/mcp/tools_core.py +69 -21
  113. package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
  114. package/src/superlocalmemory/mcp/tools_learning.py +21 -10
  115. package/src/superlocalmemory/mcp/tools_loops.py +29 -18
  116. package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
  117. package/src/superlocalmemory/mcp/tools_ops.py +115 -0
  118. package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
  119. package/src/superlocalmemory/mcp/tools_v28.py +10 -3
  120. package/src/superlocalmemory/mcp/tools_v3.py +34 -14
  121. package/src/superlocalmemory/mcp/tools_v33.py +18 -33
  122. package/src/superlocalmemory/mesh/broker.py +124 -46
  123. package/src/superlocalmemory/mesh/broker_security.py +470 -0
  124. package/src/superlocalmemory/mesh/discovery.py +365 -0
  125. package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
  126. package/src/superlocalmemory/mesh/node_identity.py +97 -0
  127. package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
  128. package/src/superlocalmemory/mesh/remote_sync.py +511 -28
  129. package/src/superlocalmemory/mesh/state_sync.py +286 -0
  130. package/src/superlocalmemory/optimize/config/store.py +45 -0
  131. package/src/superlocalmemory/parameterization/cross_project.py +12 -0
  132. package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
  133. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
  134. package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
  135. package/src/superlocalmemory/retrieval/ann_index.py +5 -0
  136. package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
  137. package/src/superlocalmemory/retrieval/engine.py +19 -4
  138. package/src/superlocalmemory/retrieval/fusion.py +4 -1
  139. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
  140. package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
  141. package/src/superlocalmemory/retrieval/reranker.py +32 -1
  142. package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
  143. package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
  144. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
  145. package/src/superlocalmemory/retrieval/vector_store.py +214 -8
  146. package/src/superlocalmemory/server/api.py +5 -5
  147. package/src/superlocalmemory/server/egress_policy.py +258 -0
  148. package/src/superlocalmemory/server/rbac_enforce.py +32 -0
  149. package/src/superlocalmemory/server/route_mutations.py +20 -0
  150. package/src/superlocalmemory/server/routes/compliance.py +153 -7
  151. package/src/superlocalmemory/server/routes/data_io.py +43 -2
  152. package/src/superlocalmemory/server/routes/events.py +15 -0
  153. package/src/superlocalmemory/server/routes/memories.py +56 -3
  154. package/src/superlocalmemory/server/routes/mesh.py +82 -1
  155. package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
  156. package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
  157. package/src/superlocalmemory/server/routes/v3_api.py +50 -27
  158. package/src/superlocalmemory/server/routes/ws.py +86 -0
  159. package/src/superlocalmemory/server/ui.py +12 -7
  160. package/src/superlocalmemory/server/unified_daemon.py +951 -120
  161. package/src/superlocalmemory/storage/_migration_internals.py +568 -0
  162. package/src/superlocalmemory/storage/_schema_version.py +110 -0
  163. package/src/superlocalmemory/storage/database.py +329 -24
  164. package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
  165. package/src/superlocalmemory/storage/erasure_fence.py +45 -0
  166. package/src/superlocalmemory/storage/generation_fence.py +63 -0
  167. package/src/superlocalmemory/storage/migration_runner.py +140 -424
  168. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
  169. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
  170. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
  171. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
  172. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
  173. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
  174. package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
  175. package/src/superlocalmemory/storage/migrations/{M034_scene_fact_members.py → M039_scene_fact_members.py} +15 -5
  176. package/src/superlocalmemory/storage/migrations/__init__.py +4 -4
  177. package/src/superlocalmemory/storage/schema.py +10 -2
  178. package/src/superlocalmemory/storage/write_coordinator.py +125 -0
  179. package/src/superlocalmemory/trust/scorer.py +28 -4
  180. package/src/superlocalmemory/ui/index.html +16 -5
  181. package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
  182. package/src/superlocalmemory/ui/js/brain.js +6 -4
  183. package/src/superlocalmemory/ui/js/compliance.js +66 -12
  184. package/src/superlocalmemory/ui/js/dashboard.js +13 -3
  185. package/src/superlocalmemory/ui/js/feedback.js +8 -2
  186. package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
  187. package/src/superlocalmemory/ui/js/modal.js +272 -5
  188. package/src/superlocalmemory/ui/js/od-backup.js +9 -2
  189. package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
  190. package/src/superlocalmemory/ui/js/od-operations.js +154 -23
  191. package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
  192. package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
  193. package/src/superlocalmemory/ui/js/od-team.js +9 -2
  194. package/src/superlocalmemory/ui/js/optimize.js +13 -16
  195. package/src/superlocalmemory/ui/js/profiles.js +7 -3
  196. package/src/superlocalmemory/ui/js/settings.js +7 -1
  197. package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
  198. package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
  199. package/src/superlocalmemory/cli/post_install.py +0 -114
  200. package/src/superlocalmemory/core/clock_monitor.py +0 -45
  201. package/src/superlocalmemory/core/db_pool.py +0 -80
  202. package/src/superlocalmemory/core/error_catalog.py +0 -113
  203. package/src/superlocalmemory/core/loop_watchdog.py +0 -56
  204. package/src/superlocalmemory/core/priority_queue.py +0 -61
  205. package/src/superlocalmemory/core/pruning_engine.py +0 -216
  206. package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
  207. package/src/superlocalmemory/core/slmignore.py +0 -125
  208. package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
  209. package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
  210. package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
  211. package/src/superlocalmemory/storage/access_control.py +0 -182
@@ -261,9 +261,13 @@ function openMemoryDetail(mem, source) {
261
261
  forgetBtn.className = 'btn btn-outline-warning btn-sm';
262
262
  forgetBtn.innerHTML = '<i class="bi bi-archive"></i> Forget';
263
263
  forgetBtn.title = 'Archive this memory — hidden from recall but recoverable';
264
- forgetBtn.onclick = function() {
265
- if (!confirm('Forget this memory? It will be archived '
266
- + '(hidden from recall but recoverable).')) return;
264
+ forgetBtn.onclick = async function() {
265
+ var confirmed = await confirmDestructive({
266
+ title: 'Forget memory',
267
+ target: mem.content ? mem.content.slice(0, 80) : 'Memory #' + mem.id,
268
+ consequence: 'Archived — hidden from recall but recoverable.',
269
+ });
270
+ if (!confirmed) return;
267
271
  forgetBtn.disabled = true;
268
272
  fetch('/api/memories/' + encodeURIComponent(mem.id) + '/forget',
269
273
  {method: 'POST'})
@@ -323,13 +327,147 @@ function openMemoryDetail(mem, source) {
323
327
  };
324
328
  actionsDiv.appendChild(mergeBtn);
325
329
 
330
+ // Task F: Set Scope — PATCH /api/memories/{fact_id}/scope
331
+ // Lets the user change personal → shared → global visibility.
332
+ var scopeBtn = document.createElement('button');
333
+ scopeBtn.className = 'btn btn-outline-secondary btn-sm';
334
+ scopeBtn.innerHTML = '<i class="bi bi-globe"></i> Set Scope…';
335
+ scopeBtn.title = 'Change memory visibility: personal, shared, or global';
336
+ (function() {
337
+ var scopeFormEl = null;
338
+ scopeBtn.addEventListener('click', function() {
339
+ // Toggle the inline scope form
340
+ if (scopeFormEl) {
341
+ scopeFormEl.remove();
342
+ scopeFormEl = null;
343
+ return;
344
+ }
345
+ scopeFormEl = document.createElement('div');
346
+ scopeFormEl.className = 'mt-2 p-2 border rounded';
347
+ scopeFormEl.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;align-items:center;width:100%';
348
+ var scopeSel = document.createElement('select');
349
+ scopeSel.className = 'form-select form-select-sm';
350
+ scopeSel.style.width = 'auto';
351
+ ['personal', 'shared', 'global'].forEach(function(s) {
352
+ var opt = document.createElement('option');
353
+ opt.value = s;
354
+ opt.textContent = s;
355
+ if (s === (mem.scope || 'personal')) opt.selected = true;
356
+ scopeSel.appendChild(opt);
357
+ });
358
+ var sharedInput = document.createElement('input');
359
+ sharedInput.className = 'form-control form-control-sm';
360
+ sharedInput.placeholder = 'shared_with (profile1,profile2)';
361
+ sharedInput.style.flex = '1';
362
+ sharedInput.style.display = scopeSel.value === 'shared' ? '' : 'none';
363
+ if (mem.shared_with) {
364
+ try {
365
+ var sw = typeof mem.shared_with === 'string'
366
+ ? JSON.parse(mem.shared_with) : mem.shared_with;
367
+ sharedInput.value = Array.isArray(sw) ? sw.join(',') : String(sw);
368
+ } catch(e) { sharedInput.value = String(mem.shared_with || ''); }
369
+ }
370
+ scopeSel.addEventListener('change', function() {
371
+ sharedInput.style.display = scopeSel.value === 'shared' ? '' : 'none';
372
+ });
373
+ var saveBtn = document.createElement('button');
374
+ saveBtn.className = 'btn btn-sm btn-primary';
375
+ saveBtn.textContent = 'Save';
376
+ saveBtn.addEventListener('click', function() {
377
+ var scope = scopeSel.value;
378
+ var sharedWith = scope === 'shared' ? sharedInput.value : '';
379
+ saveBtn.disabled = true;
380
+ saveBtn.textContent = 'Saving…';
381
+ fetch('/api/memories/' + encodeURIComponent(mem.fact_id || mem.id) + '/scope', {
382
+ method: 'PATCH',
383
+ headers: {'Content-Type': 'application/json'},
384
+ body: JSON.stringify({scope: scope, shared_with: sharedWith}),
385
+ }).then(function(r) { return r.json(); })
386
+ .then(function(d) {
387
+ if (d.success) {
388
+ mem.scope = scope;
389
+ if (typeof showToast === 'function') showToast('Scope set to ' + scope);
390
+ scopeFormEl.remove(); scopeFormEl = null;
391
+ if (typeof loadMemories === 'function') setTimeout(loadMemories, 300);
392
+ } else {
393
+ if (typeof showToast === 'function') showToast('Scope update failed: ' + (d.detail || d.error || 'unknown'));
394
+ saveBtn.disabled = false; saveBtn.textContent = 'Save';
395
+ }
396
+ }).catch(function() {
397
+ if (typeof showToast === 'function') showToast('Network error setting scope.');
398
+ saveBtn.disabled = false; saveBtn.textContent = 'Save';
399
+ });
400
+ });
401
+ scopeFormEl.appendChild(scopeSel);
402
+ scopeFormEl.appendChild(sharedInput);
403
+ scopeFormEl.appendChild(saveBtn);
404
+ actionsDiv.insertAdjacentElement('afterend', scopeFormEl);
405
+ });
406
+ }());
407
+ actionsDiv.appendChild(scopeBtn);
408
+
409
+ // Task G: Pin — POST /api/tiers/pin — keep this fact in the active tier forever
410
+ var factIdForTier = mem.fact_id || mem.id;
411
+ var pinBtn = document.createElement('button');
412
+ pinBtn.className = 'btn btn-outline-success btn-sm';
413
+ pinBtn.innerHTML = '<i class="bi bi-pin-fill"></i> Pin';
414
+ pinBtn.title = 'Pin to active tier — this fact will not be demoted by lifecycle';
415
+ pinBtn.addEventListener('click', function() {
416
+ pinBtn.disabled = true;
417
+ fetch('/api/tiers/pin', {
418
+ method: 'POST',
419
+ headers: {'Content-Type': 'application/json'},
420
+ body: JSON.stringify({fact_id: factIdForTier, reason: 'pinned from dashboard'}),
421
+ }).then(function(r) { return r.json(); })
422
+ .then(function(d) {
423
+ pinBtn.disabled = false;
424
+ if (typeof showToast === 'function') {
425
+ showToast(d && d.success ? 'Fact pinned to active tier.' : 'Pin failed: ' + (d && (d.detail || d.error) || 'unknown'));
426
+ }
427
+ }).catch(function() {
428
+ pinBtn.disabled = false;
429
+ if (typeof showToast === 'function') showToast('Network error pinning fact.');
430
+ });
431
+ });
432
+ actionsDiv.appendChild(pinBtn);
433
+
434
+ // Task G: Unpin — POST /api/tiers/unpin — allows normal tier demotion again
435
+ var unpinBtn = document.createElement('button');
436
+ unpinBtn.className = 'btn btn-outline-warning btn-sm';
437
+ unpinBtn.innerHTML = '<i class="bi bi-pin-angle"></i> Unpin';
438
+ unpinBtn.title = 'Unpin — allow normal lifecycle tier demotion';
439
+ unpinBtn.addEventListener('click', function() {
440
+ unpinBtn.disabled = true;
441
+ fetch('/api/tiers/unpin', {
442
+ method: 'POST',
443
+ headers: {'Content-Type': 'application/json'},
444
+ body: JSON.stringify({fact_id: factIdForTier, reason: ''}),
445
+ }).then(function(r) { return r.json(); })
446
+ .then(function(d) {
447
+ unpinBtn.disabled = false;
448
+ if (typeof showToast === 'function') {
449
+ showToast(d && d.success ? 'Fact unpinned — will age normally.' : 'Unpin failed: ' + (d && (d.detail || d.error) || 'unknown'));
450
+ }
451
+ }).catch(function() {
452
+ unpinBtn.disabled = false;
453
+ if (typeof showToast === 'function') showToast('Network error unpinning fact.');
454
+ });
455
+ });
456
+ actionsDiv.appendChild(unpinBtn);
457
+
326
458
  // Delete button — always available (hard delete, irreversible)
327
459
  var deleteBtn = document.createElement('button');
328
460
  deleteBtn.className = 'btn btn-outline-danger btn-sm';
329
461
  deleteBtn.innerHTML = '<i class="bi bi-trash"></i> Delete';
330
462
  deleteBtn.title = 'Permanently delete (cannot be undone) — prefer Forget';
331
- deleteBtn.onclick = function() {
332
- if (!confirm('Delete this memory? This cannot be undone.')) return;
463
+ deleteBtn.onclick = async function() {
464
+ var confirmed = await confirmDestructive({
465
+ title: 'Delete memory',
466
+ target: mem.content ? mem.content.slice(0, 80) : 'Memory #' + mem.id,
467
+ consequence: 'Permanently deleted — this cannot be undone.',
468
+ confirmLabel: 'Delete',
469
+ });
470
+ if (!confirmed) return;
333
471
  fetch('/api/memories/' + encodeURIComponent(mem.id), {method: 'DELETE'})
334
472
  .then(function(r) { return r.json(); })
335
473
  .then(function(d) {
@@ -432,6 +570,135 @@ function addDetailTagsRow(parent, label, tags) {
432
570
  parent.appendChild(dd);
433
571
  }
434
572
 
573
+ /**
574
+ * Show a shared confirmation modal for destructive dashboard actions.
575
+ * Creates the modal element on first call; reuses it on subsequent calls.
576
+ *
577
+ * @param {object} opts
578
+ * @param {string} opts.title Short header, e.g. "Delete profile"
579
+ * @param {string} opts.target Exact item being acted on, e.g. "my-project"
580
+ * @param {string} opts.consequence What happens, e.g. "Memories moved to default profile"
581
+ * @param {string} [opts.confirmLabel] Confirm button text (default: "Confirm")
582
+ * @param {string} [opts.confirmationText] Exact text required to unlock confirmation
583
+ * @returns {Promise<boolean>} Resolves true when confirmed, false when cancelled
584
+ */
585
+ var activeDestructiveConfirmation = null;
586
+
587
+ function confirmDestructive(opts) {
588
+ return new Promise(function(resolve) {
589
+ if (activeDestructiveConfirmation) {
590
+ activeDestructiveConfirmation.cancel();
591
+ }
592
+ var settled = false;
593
+ var MODAL_ID = 'slm-confirm-destructive-modal';
594
+ var modalEl = document.getElementById(MODAL_ID);
595
+
596
+ if (!modalEl) {
597
+ modalEl = document.createElement('div');
598
+ modalEl.id = MODAL_ID;
599
+ modalEl.className = 'modal fade';
600
+ modalEl.setAttribute('tabindex', '-1');
601
+ modalEl.setAttribute('aria-modal', 'true');
602
+ modalEl.setAttribute('role', 'dialog');
603
+ modalEl.innerHTML =
604
+ '<div class="modal-dialog modal-dialog-centered">' +
605
+ '<div class="modal-content">' +
606
+ '<div class="modal-header border-0 pb-1">' +
607
+ '<h5 class="modal-title slm-cd-title text-danger"></h5>' +
608
+ '<button type="button" class="btn-close"' +
609
+ ' data-slm-cd-action="cancel" aria-label="Close"></button>' +
610
+ '</div>' +
611
+ '<div class="modal-body pt-1">' +
612
+ '<p class="slm-cd-target fw-semibold mb-1"></p>' +
613
+ '<p class="slm-cd-consequence text-muted small mb-2"></p>' +
614
+ '<label class="form-label small mb-1" for="slm-cd-challenge">' +
615
+ 'Type <code class="slm-cd-confirmation-text"></code> to continue</label>' +
616
+ '<input id="slm-cd-challenge" class="form-control form-control-sm slm-cd-challenge"' +
617
+ ' type="text" autocomplete="off" spellcheck="false">' +
618
+ '</div>' +
619
+ '<div class="modal-footer border-0 pt-0">' +
620
+ '<button type="button" class="btn btn-secondary btn-sm"' +
621
+ ' data-slm-cd-action="cancel">Cancel</button>' +
622
+ '<button type="button" class="btn btn-danger btn-sm"' +
623
+ ' data-slm-cd-action="confirm">Confirm</button>' +
624
+ '</div>' +
625
+ '</div>' +
626
+ '</div>';
627
+ document.body.appendChild(modalEl);
628
+ }
629
+
630
+ var titleEl = modalEl.querySelector('.slm-cd-title');
631
+ var targetEl = modalEl.querySelector('.slm-cd-target');
632
+ var consequenceEl = modalEl.querySelector('.slm-cd-consequence');
633
+ var confirmationTextEl = modalEl.querySelector('.slm-cd-confirmation-text');
634
+ var challengeInput = modalEl.querySelector('.slm-cd-challenge');
635
+ var confirmBtn = modalEl.querySelector('[data-slm-cd-action="confirm"]');
636
+ var confirmationText = opts.confirmationText || opts.target || 'CONFIRM';
637
+
638
+ if (titleEl) titleEl.textContent = opts.title || 'Confirm action';
639
+ if (targetEl) targetEl.textContent = opts.target || '';
640
+ if (consequenceEl) consequenceEl.textContent = opts.consequence || '';
641
+ if (confirmationTextEl) confirmationTextEl.textContent = confirmationText;
642
+ if (challengeInput) challengeInput.value = '';
643
+ if (confirmBtn) {
644
+ confirmBtn.textContent = opts.confirmLabel || 'Confirm';
645
+ confirmBtn.disabled = true;
646
+ }
647
+
648
+ var bsModal = null;
649
+ if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
650
+ bsModal = bootstrap.Modal.getOrCreateInstance(modalEl);
651
+ }
652
+
653
+ function settle(result, hideModal) {
654
+ if (settled) return;
655
+ settled = true;
656
+ if (activeDestructiveConfirmation === confirmationSession) {
657
+ activeDestructiveConfirmation = null;
658
+ }
659
+ modalEl.removeEventListener('click', onAction);
660
+ if (challengeInput) challengeInput.removeEventListener('input', onChallengeInput);
661
+ if (bsModal) {
662
+ modalEl.removeEventListener('hidden.bs.modal', onHide);
663
+ modalEl.removeEventListener('shown.bs.modal', onShown);
664
+ if (hideModal !== false) bsModal.hide();
665
+ }
666
+ resolve(result);
667
+ }
668
+
669
+ function onAction(e) {
670
+ var actionEl = e.target.closest('[data-slm-cd-action]');
671
+ if (!actionEl) return;
672
+ if (actionEl.getAttribute('data-slm-cd-action') === 'confirm' &&
673
+ (!challengeInput || challengeInput.value !== confirmationText)) return;
674
+ settle(actionEl.getAttribute('data-slm-cd-action') === 'confirm');
675
+ }
676
+
677
+ function onChallengeInput() {
678
+ if (confirmBtn) confirmBtn.disabled = challengeInput.value !== confirmationText;
679
+ }
680
+
681
+ function onShown() {
682
+ if (challengeInput) challengeInput.focus();
683
+ }
684
+
685
+ function onHide() { settle(false); }
686
+
687
+ var confirmationSession = {
688
+ cancel: function() { settle(false, false); }
689
+ };
690
+ activeDestructiveConfirmation = confirmationSession;
691
+
692
+ modalEl.addEventListener('click', onAction);
693
+ if (challengeInput) challengeInput.addEventListener('input', onChallengeInput);
694
+ if (bsModal) {
695
+ modalEl.addEventListener('hidden.bs.modal', onHide, { once: true });
696
+ modalEl.addEventListener('shown.bs.modal', onShown, { once: true });
697
+ bsModal.show();
698
+ }
699
+ });
700
+ }
701
+
435
702
  function copyMemoryToClipboard() {
436
703
  if (!currentMemoryDetail) return;
437
704
  var text = currentMemoryDetail.content || currentMemoryDetail.summary || '';
@@ -642,14 +642,21 @@
642
642
  }
643
643
 
644
644
  function doDisconnect(root, destId) {
645
- if (!window.confirm('Disconnect this cloud destination? Existing backups are not deleted.')) return;
646
- authMutation('/api/backup/disconnect/' + encodeURIComponent(destId), 'DELETE')
645
+ window.confirmDestructive({
646
+ title: 'Disconnect cloud destination',
647
+ target: destId,
648
+ consequence: 'Existing backups are not deleted.',
649
+ confirmLabel: 'Disconnect',
650
+ }).then(function(confirmed) {
651
+ if (!confirmed) return;
652
+ authMutation('/api/backup/disconnect/' + encodeURIComponent(destId), 'DELETE')
647
653
  .then(function (r) { return r.json(); })
648
654
  .then(function (d) {
649
655
  toast(d.success ? 'Disconnected' : 'Disconnect failed: ' + esc(d.error || ''), !d.success);
650
656
  loadDestinations(root);
651
657
  })
652
658
  .catch(function () { toast('Disconnect failed', true); });
659
+ });
653
660
  }
654
661
 
655
662
  function openOAuth(root, oauthPath, providerLabel) {
@@ -0,0 +1,301 @@
1
+ // od-compliance-ext.js — Compliance tab extensions v1.0
2
+ // Extends the OD Operations compliance pane with:
3
+ // Task A: Entity-level GDPR erasure (POST /api/compliance/gdpr/erase-entity)
4
+ // Task B: Erasure Receipts panel (GET /api/compliance/receipts + verify)
5
+ //
6
+ // This module hooks into odRenderOperations by patching window.odRenderOperations
7
+ // to inject the new panels into the compliance tab after the base render.
8
+ //
9
+ // Security: all user-supplied strings go through esc() before innerHTML insertion.
10
+ // Confirm guard on entity erase (typed confirmation + confirmDestructive).
11
+ //
12
+ // Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
13
+
14
+ /* global window, document, fetch, Promise */
15
+ (function () {
16
+ 'use strict';
17
+
18
+ // ─── Helpers ──────────────────────────────────────────────────────────────
19
+
20
+ function esc(s) {
21
+ return String(s == null ? '' : s)
22
+ .replace(/&/g, '&amp;')
23
+ .replace(/</g, '&lt;')
24
+ .replace(/>/g, '&gt;')
25
+ .replace(/"/g, '&quot;')
26
+ .replace(/'/g, '&#39;');
27
+ }
28
+
29
+ function fmtTime(ts) {
30
+ if (!ts) return '—';
31
+ try {
32
+ var d = typeof ts === 'number' ? new Date(ts * 1000) : new Date(ts);
33
+ return d.toLocaleString();
34
+ } catch (e) { return String(ts); }
35
+ }
36
+
37
+ // ─── Task A: Entity Erase Card ────────────────────────────────────────────
38
+
39
+ var ENTITY_ERASE_ID = 'od-entity-erase-card';
40
+
41
+ function buildEntityEraseCard() {
42
+ var card = document.createElement('div');
43
+ card.id = ENTITY_ERASE_ID;
44
+ card.className = 'card';
45
+ card.style.marginTop = '16px';
46
+ // Using textContent / controlled HTML (only static strings + esc'd values below)
47
+ card.innerHTML =
48
+ '<div class="card-head">' +
49
+ '<h3>Entity-level GDPR erasure</h3>' +
50
+ '</div>' +
51
+ '<div class="card-pad">' +
52
+ '<p class="muted" style="font-size:13px" id="od-entity-erase-summary">' +
53
+ 'Erase all facts mentioning a specific named entity (GDPR Art. 17). ' +
54
+ 'Enter the entity name exactly; you will need to confirm it. This is IRREVERSIBLE.' +
55
+ '</p>' +
56
+ '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px">' +
57
+ '<input id="od-entity-erase-input" type="text" maxlength="200" ' +
58
+ 'placeholder="Entity name…" ' +
59
+ 'style="flex:1;min-width:180px;padding:7px 10px;border-radius:8px;' +
60
+ 'border:1px solid var(--border);background:var(--card);color:var(--fg);font-size:13px">' +
61
+ '<button id="od-entity-erase-btn" class="btn sm" ' +
62
+ 'style="border-color:#ff6b6b;color:#ff6b6b">Erase entity…</button>' +
63
+ '</div>' +
64
+ '</div>';
65
+ return card;
66
+ }
67
+
68
+ function wireEntityEraseCard() {
69
+ var btn = document.getElementById('od-entity-erase-btn');
70
+ var input = document.getElementById('od-entity-erase-input');
71
+ var summary = document.getElementById('od-entity-erase-summary');
72
+ if (!btn || !input) return;
73
+
74
+ btn.addEventListener('click', function () {
75
+ var entityName = (input.value || '').trim();
76
+ if (!entityName) {
77
+ if (summary) summary.textContent = 'Enter an entity name first.';
78
+ return;
79
+ }
80
+ // Step 1: typed-confirmation prompt (same guard as profile erase)
81
+ var typed = window.prompt(
82
+ 'This PERMANENTLY erases all facts mentioning entity "' + entityName + '".\n' +
83
+ 'This cannot be undone.\n\nType the entity name to confirm:'
84
+ );
85
+ if (typed === null) return; // cancelled
86
+ if (typed !== entityName) {
87
+ if (summary) summary.textContent = 'Erase cancelled — name did not match.';
88
+ return;
89
+ }
90
+ // Step 2: confirmDestructive modal
91
+ window.confirmDestructive({
92
+ title: 'Erase entity',
93
+ target: entityName,
94
+ consequence: 'All facts mentioning this entity will be permanently erased.',
95
+ confirmLabel: 'Erase entity',
96
+ }).then(function (confirmed) {
97
+ if (!confirmed) return;
98
+ btn.disabled = true;
99
+ btn.textContent = 'Erasing…';
100
+ if (summary) summary.textContent = 'Erasing entity "' + esc(entityName) + '"…';
101
+ fetch('/api/compliance/gdpr/erase-entity', {
102
+ method: 'POST',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify({ entity_name: entityName, confirm: entityName }),
105
+ })
106
+ .then(function (r) { return r.json(); })
107
+ .then(function (d) {
108
+ if (!summary) return;
109
+ if (d && d.success) {
110
+ var parts = [];
111
+ if (d.facts != null) parts.push('facts erased: ' + d.facts);
112
+ if (d.entity != null) parts.push('entity: ' + esc(String(d.entity)));
113
+ summary.textContent = 'Entity erased — ' + (parts.join(', ') || 'done') + '.';
114
+ input.value = '';
115
+ // Reload receipts panel to reflect new receipt
116
+ loadReceiptsPanel();
117
+ } else {
118
+ summary.textContent = 'Erase failed: ' + esc(String((d && d.error) || 'unknown'));
119
+ }
120
+ })
121
+ .catch(function () {
122
+ if (summary) summary.textContent = 'Erase request failed. Check console.';
123
+ })
124
+ .finally(function () {
125
+ btn.disabled = false;
126
+ btn.textContent = 'Erase entity…';
127
+ });
128
+ });
129
+ });
130
+ }
131
+
132
+ // ─── Task B: Erasure Receipts Panel ───────────────────────────────────────
133
+
134
+ var RECEIPTS_ID = 'od-receipts-card';
135
+
136
+ function buildReceiptsCard() {
137
+ var card = document.createElement('div');
138
+ card.id = RECEIPTS_ID;
139
+ card.className = 'card';
140
+ card.style.marginTop = '16px';
141
+ card.innerHTML =
142
+ '<div class="card-head">' +
143
+ '<h3>Erasure receipts</h3>' +
144
+ '<div class="spacer"></div>' +
145
+ '<button class="btn sm" id="od-receipts-refresh-btn">Refresh</button>' +
146
+ '</div>' +
147
+ '<div class="card-pad">' +
148
+ '<table class="tbl" id="od-receipts-tbl">' +
149
+ '<thead><tr>' +
150
+ '<th>ID</th>' +
151
+ '<th>Type</th>' +
152
+ '<th>Subject</th>' +
153
+ '<th>State</th>' +
154
+ '<th>Facts</th>' +
155
+ '<th>Requested</th>' +
156
+ '<th></th>' +
157
+ '</tr></thead>' +
158
+ '<tbody id="od-receipts-body">' +
159
+ '<tr><td colspan="7" class="dim" style="text-align:center;padding:20px">Loading…</td></tr>' +
160
+ '</tbody>' +
161
+ '</table>' +
162
+ '</div>';
163
+ return card;
164
+ }
165
+
166
+ function loadReceiptsPanel() {
167
+ var tbody = document.getElementById('od-receipts-body');
168
+ if (!tbody) return;
169
+ tbody.innerHTML = '<tr><td colspan="7" class="dim" style="text-align:center;padding:16px">Loading…</td></tr>';
170
+ fetch('/api/compliance/receipts?limit=50')
171
+ .then(function (r) { return r.ok ? r.json() : null; })
172
+ .then(function (d) {
173
+ if (!d || !d.available) {
174
+ tbody.innerHTML = '<tr><td colspan="7" class="dim" style="text-align:center;padding:16px">Receipts unavailable.</td></tr>';
175
+ return;
176
+ }
177
+ var receipts = d.receipts || [];
178
+ if (receipts.length === 0) {
179
+ tbody.innerHTML = '<tr><td colspan="7" class="dim" style="text-align:center;padding:16px">No erasure receipts yet.</td></tr>';
180
+ return;
181
+ }
182
+ tbody.innerHTML = receipts.map(function (r) {
183
+ var shortId = esc(String(r.erasure_id || '').slice(0, 8)) + '…';
184
+ var stateClass = r.state === 'COMPLETE' ? 'ok' : 'danger';
185
+ var allOk = r.all_erased ? '<span class="badge ok">all erased</span>' : '<span class="badge warn">partial</span>';
186
+ return (
187
+ '<tr>' +
188
+ '<td class="mono dim" style="font-size:11px" title="' + esc(r.erasure_id || '') + '">' + shortId + '</td>' +
189
+ '<td><span class="badge neutral">' + esc(r.subject_type || '') + '</span></td>' +
190
+ '<td class="dim" style="font-size:12px;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' +
191
+ esc(r.subject_id || '') +
192
+ '</td>' +
193
+ '<td><span class="badge ' + stateClass + '">' + esc(r.state || '') + '</span></td>' +
194
+ '<td class="num dim">' + esc(String(r.fact_count || 0)) + '</td>' +
195
+ '<td class="dim" style="font-size:11px">' + esc(fmtTime(r.requested_at)) + '</td>' +
196
+ '<td><button class="btn sm ghost" data-verify-id="' + esc(r.erasure_id || '') + '">Verify</button></td>' +
197
+ '</tr>'
198
+ );
199
+ }).join('');
200
+ // Wire verify buttons
201
+ tbody.querySelectorAll('[data-verify-id]').forEach(function (btn) {
202
+ btn.addEventListener('click', function () {
203
+ var eid = btn.getAttribute('data-verify-id');
204
+ btn.disabled = true;
205
+ btn.textContent = '…';
206
+ fetch('/api/compliance/receipts/' + encodeURIComponent(eid) + '/verify')
207
+ .then(function (r) { return r.ok ? r.json() : null; })
208
+ .then(function (d) {
209
+ if (d && d.verified === true) {
210
+ btn.textContent = 'Verified ✓';
211
+ btn.style.color = 'var(--ok)';
212
+ } else if (d && d.verified === false) {
213
+ btn.textContent = 'Tampered!';
214
+ btn.style.color = 'var(--danger)';
215
+ } else {
216
+ btn.textContent = 'Error';
217
+ }
218
+ btn.disabled = false;
219
+ })
220
+ .catch(function () { btn.textContent = 'Error'; btn.disabled = false; });
221
+ });
222
+ });
223
+ })
224
+ .catch(function () {
225
+ var tb = document.getElementById('od-receipts-body');
226
+ if (tb) tb.innerHTML = '<tr><td colspan="7" class="dim" style="text-align:center;padding:16px">Failed to load receipts.</td></tr>';
227
+ });
228
+ }
229
+
230
+ function wireReceiptsPanel() {
231
+ var refreshBtn = document.getElementById('od-receipts-refresh-btn');
232
+ if (refreshBtn) {
233
+ refreshBtn.addEventListener('click', loadReceiptsPanel);
234
+ }
235
+ loadReceiptsPanel();
236
+ }
237
+
238
+ // ─── Injection Hook ───────────────────────────────────────────────────────
239
+
240
+ /**
241
+ * Inject the two new panels into the compliance tab section.
242
+ * Called after odRenderOperations() has built and populated the DOM.
243
+ */
244
+ function injectCompliancePanels(container) {
245
+ // Find the compliance tabpane
246
+ var pane = container && container.querySelector('[data-od-pane="compliance"]');
247
+ if (!pane) return;
248
+
249
+ // Remove any stale injected panels from a previous render
250
+ var oldErase = pane.querySelector('#' + ENTITY_ERASE_ID);
251
+ if (oldErase) oldErase.remove();
252
+ var oldReceipts = pane.querySelector('#' + RECEIPTS_ID);
253
+ if (oldReceipts) oldReceipts.remove();
254
+
255
+ // Append the two new cards at the bottom of the compliance tab
256
+ pane.appendChild(buildEntityEraseCard());
257
+ pane.appendChild(buildReceiptsCard());
258
+
259
+ // Wire interactions
260
+ wireEntityEraseCard();
261
+ wireReceiptsPanel();
262
+ }
263
+
264
+ // ─── Patch odRenderOperations ─────────────────────────────────────────────
265
+
266
+ /**
267
+ * Wait for od-operations.js to define window.odRenderOperations, then wrap it
268
+ * so our panels are injected after every render. Uses polling with a cap —
269
+ * no unbounded loops.
270
+ */
271
+ function patchOdRenderOperations() {
272
+ var base = window.odRenderOperations;
273
+ if (typeof base !== 'function') return false;
274
+ window.odRenderOperations = function (container) {
275
+ base(container);
276
+ // od-operations.js renders asynchronously (Promise.all inside).
277
+ // Give it time to populate the DOM before injecting our panels.
278
+ setTimeout(function () { injectCompliancePanels(container); }, 600);
279
+ };
280
+ return true;
281
+ }
282
+
283
+ // Retry for up to 3s in case od-operations.js hasn't defined the function yet.
284
+ var _attempts = 0;
285
+ function tryPatch() {
286
+ if (patchOdRenderOperations()) return;
287
+ _attempts += 1;
288
+ if (_attempts < 30) setTimeout(tryPatch, 100);
289
+ }
290
+
291
+ // Also inject into the currently active operations pane on DOMContentLoaded
292
+ document.addEventListener('DOMContentLoaded', function () {
293
+ tryPatch();
294
+ // If the operations pane is already in the DOM and populated, inject now
295
+ var pane = document.getElementById('operations-pane');
296
+ if (pane && pane.querySelector('[data-od-pane="compliance"]')) {
297
+ injectCompliancePanels(pane);
298
+ }
299
+ });
300
+
301
+ }());