superlocalmemory 3.8.13 → 4.0.0

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 (212) hide show
  1. package/ATTRIBUTION.md +4 -4
  2. package/CHANGELOG.md +113 -121
  3. package/README.md +65 -63
  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 +36 -9
  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/scene_builder.py +115 -13
  86. package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
  87. package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
  88. package/src/superlocalmemory/evolution/evolution_store.py +244 -4
  89. package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
  90. package/src/superlocalmemory/evolution/model_selection.py +18 -3
  91. package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
  92. package/src/superlocalmemory/evolution/skill_activator.py +270 -0
  93. package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
  94. package/src/superlocalmemory/evolution/types.py +30 -8
  95. package/src/superlocalmemory/graph/cozo_backend.py +17 -9
  96. package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
  97. package/src/superlocalmemory/hooks/auto_recall.py +64 -30
  98. package/src/superlocalmemory/hooks/codex_assets.py +14 -1
  99. package/src/superlocalmemory/infra/backup.py +434 -7
  100. package/src/superlocalmemory/infra/process_reaper.py +18 -0
  101. package/src/superlocalmemory/infra/self_heal.py +401 -0
  102. package/src/superlocalmemory/learning/feedback.py +52 -9
  103. package/src/superlocalmemory/loops/engine.py +10 -0
  104. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  105. package/src/superlocalmemory/mcp/http_transport.py +30 -331
  106. package/src/superlocalmemory/mcp/profiles.py +5 -0
  107. package/src/superlocalmemory/mcp/resources.py +8 -0
  108. package/src/superlocalmemory/mcp/server.py +51 -4
  109. package/src/superlocalmemory/mcp/shared.py +19 -0
  110. package/src/superlocalmemory/mcp/tools_active.py +25 -4
  111. package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
  112. package/src/superlocalmemory/mcp/tools_context.py +50 -8
  113. package/src/superlocalmemory/mcp/tools_core.py +69 -21
  114. package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
  115. package/src/superlocalmemory/mcp/tools_learning.py +21 -10
  116. package/src/superlocalmemory/mcp/tools_loops.py +29 -18
  117. package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
  118. package/src/superlocalmemory/mcp/tools_ops.py +115 -0
  119. package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
  120. package/src/superlocalmemory/mcp/tools_v28.py +10 -3
  121. package/src/superlocalmemory/mcp/tools_v3.py +34 -14
  122. package/src/superlocalmemory/mcp/tools_v33.py +18 -33
  123. package/src/superlocalmemory/mesh/broker.py +124 -46
  124. package/src/superlocalmemory/mesh/broker_security.py +470 -0
  125. package/src/superlocalmemory/mesh/discovery.py +365 -0
  126. package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
  127. package/src/superlocalmemory/mesh/node_identity.py +97 -0
  128. package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
  129. package/src/superlocalmemory/mesh/remote_sync.py +511 -28
  130. package/src/superlocalmemory/mesh/state_sync.py +286 -0
  131. package/src/superlocalmemory/optimize/config/store.py +45 -0
  132. package/src/superlocalmemory/parameterization/cross_project.py +12 -0
  133. package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
  134. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
  135. package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
  136. package/src/superlocalmemory/retrieval/ann_index.py +5 -0
  137. package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
  138. package/src/superlocalmemory/retrieval/engine.py +19 -4
  139. package/src/superlocalmemory/retrieval/fusion.py +4 -1
  140. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
  141. package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
  142. package/src/superlocalmemory/retrieval/reranker.py +32 -1
  143. package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
  144. package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
  145. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
  146. package/src/superlocalmemory/retrieval/vector_store.py +214 -8
  147. package/src/superlocalmemory/server/api.py +5 -5
  148. package/src/superlocalmemory/server/egress_policy.py +258 -0
  149. package/src/superlocalmemory/server/rbac_enforce.py +32 -0
  150. package/src/superlocalmemory/server/route_mutations.py +20 -0
  151. package/src/superlocalmemory/server/routes/compliance.py +153 -7
  152. package/src/superlocalmemory/server/routes/data_io.py +43 -2
  153. package/src/superlocalmemory/server/routes/events.py +15 -0
  154. package/src/superlocalmemory/server/routes/memories.py +56 -3
  155. package/src/superlocalmemory/server/routes/mesh.py +82 -1
  156. package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
  157. package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
  158. package/src/superlocalmemory/server/routes/v3_api.py +50 -27
  159. package/src/superlocalmemory/server/routes/ws.py +86 -0
  160. package/src/superlocalmemory/server/ui.py +6 -6
  161. package/src/superlocalmemory/server/unified_daemon.py +942 -119
  162. package/src/superlocalmemory/storage/_migration_internals.py +568 -0
  163. package/src/superlocalmemory/storage/_schema_version.py +110 -0
  164. package/src/superlocalmemory/storage/database.py +329 -24
  165. package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
  166. package/src/superlocalmemory/storage/erasure_fence.py +45 -0
  167. package/src/superlocalmemory/storage/generation_fence.py +63 -0
  168. package/src/superlocalmemory/storage/migration_runner.py +140 -417
  169. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
  170. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
  171. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
  172. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
  173. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
  174. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
  175. package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
  176. package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
  177. package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
  178. package/src/superlocalmemory/storage/schema.py +67 -0
  179. package/src/superlocalmemory/storage/write_coordinator.py +125 -0
  180. package/src/superlocalmemory/trust/scorer.py +28 -4
  181. package/src/superlocalmemory/ui/index.html +14 -3
  182. package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
  183. package/src/superlocalmemory/ui/js/brain.js +6 -4
  184. package/src/superlocalmemory/ui/js/compliance.js +66 -12
  185. package/src/superlocalmemory/ui/js/dashboard.js +13 -3
  186. package/src/superlocalmemory/ui/js/feedback.js +8 -2
  187. package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
  188. package/src/superlocalmemory/ui/js/modal.js +272 -5
  189. package/src/superlocalmemory/ui/js/od-backup.js +9 -2
  190. package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
  191. package/src/superlocalmemory/ui/js/od-operations.js +154 -23
  192. package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
  193. package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
  194. package/src/superlocalmemory/ui/js/od-team.js +9 -2
  195. package/src/superlocalmemory/ui/js/optimize.js +13 -16
  196. package/src/superlocalmemory/ui/js/profiles.js +7 -3
  197. package/src/superlocalmemory/ui/js/settings.js +7 -1
  198. package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
  199. package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
  200. package/src/superlocalmemory/cli/post_install.py +0 -114
  201. package/src/superlocalmemory/core/clock_monitor.py +0 -45
  202. package/src/superlocalmemory/core/db_pool.py +0 -80
  203. package/src/superlocalmemory/core/error_catalog.py +0 -113
  204. package/src/superlocalmemory/core/loop_watchdog.py +0 -56
  205. package/src/superlocalmemory/core/priority_queue.py +0 -61
  206. package/src/superlocalmemory/core/pruning_engine.py +0 -216
  207. package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
  208. package/src/superlocalmemory/core/slmignore.py +0 -125
  209. package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
  210. package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
  211. package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
  212. package/src/superlocalmemory/storage/access_control.py +0 -182
@@ -10,11 +10,9 @@ async function loadCompliance() {
10
10
  var filterValue = filterEl ? filterEl.value : '';
11
11
 
12
12
  try {
13
- var url = '/api/compliance/status';
14
- if (filterValue) url += '?event_type=' + encodeURIComponent(filterValue);
15
- var response = await fetch(url);
16
- if (!response.ok) throw new Error('HTTP ' + response.status);
17
- var data = await response.json();
13
+ var statusResp = await fetch('/api/compliance/status');
14
+ if (!statusResp.ok) throw new Error('HTTP ' + statusResp.status);
15
+ var data = await statusResp.json();
18
16
  _complianceData = data;
19
17
 
20
18
  if (!data.available) {
@@ -24,20 +22,37 @@ async function loadCompliance() {
24
22
 
25
23
  renderComplianceStats(data);
26
24
  renderCompliancePolicies(data);
27
- renderComplianceAudit(data);
25
+
26
+ // Task C: fetch real audit trail from dedicated endpoint with filters
27
+ var auditUrl = '/api/compliance/audit?limit=100';
28
+ if (filterValue) auditUrl += '&event_type=' + encodeURIComponent(filterValue);
29
+ var auditResp = await fetch(auditUrl);
30
+ var auditData = auditResp.ok ? await auditResp.json() : null;
31
+ renderComplianceAudit(auditData || data);
28
32
 
29
33
  var badge = document.getElementById('compliance-profile-badge');
30
34
  if (badge) badge.textContent = data.active_profile || 'default';
35
+
36
+ // Task C: show chain-integrity indicator
37
+ var chainEl = document.getElementById('cp-chain-integrity');
38
+ if (chainEl && auditData) {
39
+ var ok = auditData.chain_verified !== false;
40
+ chainEl.innerHTML = ok
41
+ ? '<span class="badge bg-success"><i class="bi bi-shield-check"></i> Chain verified</span>'
42
+ : '<span class="badge bg-danger"><i class="bi bi-shield-x"></i> Chain integrity issue</span>';
43
+ }
31
44
  } catch (error) {
32
45
  console.error('Error loading compliance:', error);
33
46
  }
34
47
  }
35
48
 
49
+ // Task D: Fix KPI field reads — /api/compliance/status returns top-level fields,
50
+ // NOT a nested .stats object. audit_events_count, retention_policies (array),
51
+ // abac_policies_count are all top-level.
36
52
  function renderComplianceStats(data) {
37
- var stats = data.stats || {};
38
- animateCounter('cp-audit-count', stats.audit_count || 0);
39
- animateCounter('cp-retention-count', stats.retention_count || 0);
40
- animateCounter('cp-abac-count', stats.abac_count || 0);
53
+ animateCounter('cp-audit-count', data.audit_events_count || 0);
54
+ animateCounter('cp-retention-count', (data.retention_policies || []).length);
55
+ animateCounter('cp-abac-count', data.abac_policies_count || 0);
41
56
  }
42
57
 
43
58
  function renderCompliancePolicies(data) {
@@ -58,7 +73,7 @@ function renderCompliancePolicies(data) {
58
73
  table.className = 'table table-sm table-hover mb-0';
59
74
  var thead = document.createElement('thead');
60
75
  var headRow = document.createElement('tr');
61
- ['Policy Name', 'Retention (days)', 'Category', 'Action', 'Created'].forEach(function(h) {
76
+ ['Policy Name', 'Retention (days)', 'Category', 'Action', 'Created', ''].forEach(function(h) {
62
77
  var th = document.createElement('th');
63
78
  th.textContent = h;
64
79
  headRow.appendChild(th);
@@ -106,16 +121,55 @@ function renderCompliancePolicies(data) {
106
121
  dateCell.textContent = formatDate(pol.created_at || '');
107
122
  row.appendChild(dateCell);
108
123
 
124
+ // Task E: Delete button per row — calls DELETE /api/compliance/retention-policy?name=
125
+ var actCell = document.createElement('td');
126
+ var delBtn = document.createElement('button');
127
+ delBtn.className = 'btn btn-sm btn-outline-danger';
128
+ delBtn.textContent = 'Delete';
129
+ delBtn.addEventListener('click', (function(policyName) {
130
+ return async function() {
131
+ var confirmed = await confirmDestructive({
132
+ title: 'Delete retention policy',
133
+ target: policyName,
134
+ consequence: 'This policy will stop applying to all memories.',
135
+ confirmLabel: 'Delete',
136
+ });
137
+ if (!confirmed) return;
138
+ delBtn.disabled = true;
139
+ try {
140
+ var r = await fetch(
141
+ '/api/compliance/retention-policy?name=' + encodeURIComponent(policyName),
142
+ { method: 'DELETE' }
143
+ );
144
+ var d = await r.json().catch(function() { return {}; });
145
+ if (d.success !== false) {
146
+ showToast('Policy deleted.');
147
+ loadCompliance();
148
+ } else {
149
+ showToast((d.error || 'Delete failed.'));
150
+ delBtn.disabled = false;
151
+ }
152
+ } catch (e) {
153
+ showToast('Network error deleting policy.');
154
+ delBtn.disabled = false;
155
+ }
156
+ };
157
+ }(pol.name || '')));
158
+ actCell.appendChild(delBtn);
159
+ row.appendChild(actCell);
160
+
109
161
  tbody.appendChild(row);
110
162
  }
111
163
  table.appendChild(tbody);
112
164
  container.appendChild(table);
113
165
  }
114
166
 
167
+ // Task C: renderComplianceAudit accepts data from either the status endpoint
168
+ // (recent_audit_events) or the dedicated audit endpoint (events). Tries both.
115
169
  function renderComplianceAudit(data) {
116
170
  var container = document.getElementById('compliance-audit-content');
117
171
  if (!container) return;
118
- var events = data.audit_events || [];
172
+ var events = data.events || data.recent_audit_events || data.audit_events || [];
119
173
  container.textContent = '';
120
174
 
121
175
  if (events.length === 0) {
@@ -354,11 +354,21 @@ async function loadDashboard() {
354
354
  if (dashVer) dashVer.textContent = ver;
355
355
  if (settVer) settVer.textContent = ver;
356
356
 
357
- // OD dashboard subtitle
357
+ // OD dashboard subtitle — locality comes from the mode record via API
358
+ // (data.data_locality_label). Never hardcode a fixed locality claim;
359
+ // Mode C is provider-assisted and must not claim data never leaves.
358
360
  var subtitle = document.getElementById('od-dash-subtitle');
359
361
  if (subtitle && data.mode_name) {
360
- subtitle.textContent = 'Mode ' + data.mode.toUpperCase() + ' · ' + data.mode_name +
361
- ' · local-only · v' + (ver || '?');
362
+ var locality = (data.data_locality_label || '').trim();
363
+ var parts = [
364
+ 'Mode ' + data.mode.toUpperCase(),
365
+ data.mode_name,
366
+ ];
367
+ if (locality) {
368
+ parts.push(locality);
369
+ }
370
+ parts.push('v' + (ver || '?'));
371
+ subtitle.textContent = parts.join(' · ');
362
372
  }
363
373
 
364
374
  // Update mode badge in sidebar (ng-premount hidden element)
@@ -312,8 +312,14 @@ function showPrivacyDetails() {
312
312
  /**
313
313
  * Reset all learning data.
314
314
  */
315
- function resetLearningData() {
316
- if (!confirm('Reset all learning data? Your memories will be preserved.')) return;
315
+ async function resetLearningData() {
316
+ var confirmed = await confirmDestructive({
317
+ title: 'Reset learning data',
318
+ target: 'All learned patterns and ranking signals',
319
+ consequence: 'Your memories will be preserved.',
320
+ confirmLabel: 'Reset',
321
+ });
322
+ if (!confirmed) return;
317
323
 
318
324
  fetch('/api/learning/reset', {method: 'POST'})
319
325
  .then(function(r) { return r.json(); })
@@ -299,7 +299,13 @@ async function compactDryRun() {
299
299
  }
300
300
 
301
301
  async function compactExecute() {
302
- if (!confirm('This will transition memories to lower lifecycle states. Continue?')) return;
302
+ var confirmed = await confirmDestructive({
303
+ title: 'Apply compaction',
304
+ target: 'All eligible memories',
305
+ consequence: 'Transitions memories to lower lifecycle states.',
306
+ confirmLabel: 'Apply',
307
+ });
308
+ if (!confirmed) return;
303
309
  try {
304
310
  var response = await fetch('/api/lifecycle/compact', {
305
311
  method: 'POST',
@@ -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) {