superlocalmemory 3.7.7 → 3.8.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 (262) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +85 -0
  4. package/README.md +199 -29
  5. package/package.json +4 -2
  6. package/plugin/.claude-plugin/plugin.json +2 -2
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/agents/slm-governance-advisor.md +80 -0
  30. package/plugin-src/agents/slm-loop-runner.md +71 -0
  31. package/plugin-src/agents/slm-memory-advisor.md +10 -5
  32. package/plugin-src/agents/slm-optimize-advisor.md +9 -3
  33. package/plugin-src/commands/slm-loop.md +31 -0
  34. package/plugin-src/hooks/hooks.json +79 -0
  35. package/plugin-src/manifest.json +7 -2
  36. package/plugin-src/requirements.txt +1 -1
  37. package/plugin-src/rules/AGENTS.md +57 -18
  38. package/plugin-src/rules/CLAUDE.md.fragment +8 -8
  39. package/plugin-src/scripts/slm-launch +46 -7
  40. package/plugin-src/settings.json +9 -0
  41. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  42. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  43. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  45. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  46. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  47. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  48. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  49. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  50. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  51. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  52. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  53. package/pyproject.toml +1 -2
  54. package/scripts/postinstall/validation.js +2 -0
  55. package/scripts/postinstall-interactive.js +74 -2
  56. package/src/superlocalmemory/__init__.py +1 -1
  57. package/src/superlocalmemory/access/__init__.py +3 -0
  58. package/src/superlocalmemory/access/rbac.py +477 -0
  59. package/src/superlocalmemory/cli/commands.py +96 -12
  60. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  61. package/src/superlocalmemory/cli/loop_cmd.py +192 -0
  62. package/src/superlocalmemory/cli/main.py +39 -4
  63. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  64. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  65. package/src/superlocalmemory/cli/pending_store.py +49 -13
  66. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  67. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  68. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  69. package/src/superlocalmemory/compliance/audit.py +6 -0
  70. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  71. package/src/superlocalmemory/compliance/retention.py +176 -45
  72. package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
  73. package/src/superlocalmemory/core/community_summary.py +267 -0
  74. package/src/superlocalmemory/core/config.py +216 -3
  75. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  76. package/src/superlocalmemory/core/context_cache.py +61 -18
  77. package/src/superlocalmemory/core/embedding_worker.py +17 -2
  78. package/src/superlocalmemory/core/embeddings.py +12 -1
  79. package/src/superlocalmemory/core/engine.py +17 -1
  80. package/src/superlocalmemory/core/engine_ingestion.py +29 -0
  81. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  82. package/src/superlocalmemory/core/entity_community.py +178 -0
  83. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  84. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  85. package/src/superlocalmemory/core/key_expander.py +138 -0
  86. package/src/superlocalmemory/core/maintenance.py +23 -0
  87. package/src/superlocalmemory/core/modes.py +1 -1
  88. package/src/superlocalmemory/core/mutations.py +2 -2
  89. package/src/superlocalmemory/core/pii.py +105 -0
  90. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  91. package/src/superlocalmemory/core/recall_pipeline.py +2 -0
  92. package/src/superlocalmemory/core/recall_worker.py +20 -6
  93. package/src/superlocalmemory/core/scale_engine.py +60 -1
  94. package/src/superlocalmemory/core/security_primitives.py +40 -2
  95. package/src/superlocalmemory/core/store_pipeline.py +35 -11
  96. package/src/superlocalmemory/core/worker_pool.py +21 -6
  97. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  98. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  99. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  100. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  101. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  102. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  103. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  104. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  105. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  106. package/src/superlocalmemory/evolution/triggers.py +22 -13
  107. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  108. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  109. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  110. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  111. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  112. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  113. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  114. package/src/superlocalmemory/hooks/portable_kit.py +114 -1
  115. package/src/superlocalmemory/infra/auth_middleware.py +28 -0
  116. package/src/superlocalmemory/infra/backup.py +12 -1
  117. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  118. package/src/superlocalmemory/infra/data_root.py +43 -4
  119. package/src/superlocalmemory/infra/event_bus.py +107 -24
  120. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  121. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  122. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  123. package/src/superlocalmemory/learning/cross_project.py +28 -19
  124. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  125. package/src/superlocalmemory/loops/__init__.py +56 -0
  126. package/src/superlocalmemory/loops/budget.py +58 -0
  127. package/src/superlocalmemory/loops/engine.py +164 -0
  128. package/src/superlocalmemory/loops/ledger.py +243 -0
  129. package/src/superlocalmemory/loops/models.py +152 -0
  130. package/src/superlocalmemory/loops/rules.py +52 -0
  131. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  132. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  133. package/src/superlocalmemory/mcp/profiles.py +103 -0
  134. package/src/superlocalmemory/mcp/server.py +21 -49
  135. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  136. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  137. package/src/superlocalmemory/mcp/tools_core.py +50 -5
  138. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  139. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  140. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  141. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  142. package/src/superlocalmemory/mesh/broker.py +237 -129
  143. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  144. package/src/superlocalmemory/optimize/NOTICE +1 -6
  145. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  147. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  148. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  149. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  150. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  151. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  152. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  153. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  154. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  155. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  156. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  157. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  158. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  159. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  160. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  161. package/src/superlocalmemory/retrieval/engine.py +168 -26
  162. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  163. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  164. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  165. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  166. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  167. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  168. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  169. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  170. package/src/superlocalmemory/server/api.py +21 -4
  171. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  172. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  173. package/src/superlocalmemory/server/recall_health.py +24 -3
  174. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  175. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  176. package/src/superlocalmemory/server/routes/agents.py +128 -38
  177. package/src/superlocalmemory/server/routes/backup.py +34 -10
  178. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  179. package/src/superlocalmemory/server/routes/brain.py +21 -5
  180. package/src/superlocalmemory/server/routes/chat.py +72 -16
  181. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  182. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  183. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  184. package/src/superlocalmemory/server/routes/entity.py +9 -4
  185. package/src/superlocalmemory/server/routes/events.py +24 -8
  186. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  187. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  188. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  189. package/src/superlocalmemory/server/routes/insights.py +3 -3
  190. package/src/superlocalmemory/server/routes/learning.py +14 -14
  191. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  192. package/src/superlocalmemory/server/routes/memories.py +221 -49
  193. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  194. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  195. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  196. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  197. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  198. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  199. package/src/superlocalmemory/server/routes/stats.py +13 -6
  200. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  201. package/src/superlocalmemory/server/routes/v3_api.py +194 -81
  202. package/src/superlocalmemory/server/routes/ws.py +5 -2
  203. package/src/superlocalmemory/server/security_middleware.py +12 -5
  204. package/src/superlocalmemory/server/ui.py +30 -5
  205. package/src/superlocalmemory/server/unified_daemon.py +431 -75
  206. package/src/superlocalmemory/server/write_identity.py +38 -8
  207. package/src/superlocalmemory/storage/database.py +265 -53
  208. package/src/superlocalmemory/storage/migration_runner.py +53 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/models.py +4 -0
  217. package/src/superlocalmemory/storage/schema.py +87 -0
  218. package/src/superlocalmemory/storage/schema_v32.py +0 -9
  219. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  220. package/src/superlocalmemory/trust/gate.py +49 -8
  221. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  222. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  223. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  224. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  225. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  226. package/src/superlocalmemory/ui/favicon.svg +35 -4
  227. package/src/superlocalmemory/ui/index.html +306 -173
  228. package/src/superlocalmemory/ui/js/brain.js +5 -20
  229. package/src/superlocalmemory/ui/js/core.js +47 -31
  230. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  231. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  232. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  233. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  234. package/src/superlocalmemory/ui/js/memories.js +15 -4
  235. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  236. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  237. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  238. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  239. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  240. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  241. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  242. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  243. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  244. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  245. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  246. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  247. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  248. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  249. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  250. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  251. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  252. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  253. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  254. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  255. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  256. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  257. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  258. package/src/superlocalmemory/ui/js/settings.js +2 -2
  259. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  260. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  261. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  262. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -21,7 +21,8 @@
21
21
  (() => {
22
22
  'use strict';
23
23
 
24
- const TOKEN_STORAGE_KEY = 'slm_install_token';
24
+ // B2 (3.7.9): token kept in a private closure var, never sessionStorage.
25
+ let _tokenCache = null;
25
26
 
26
27
  // --------------------------------------------------------------------
27
28
  // Safe DOM helper — the ONLY way this file creates nodes.
@@ -69,25 +70,9 @@
69
70
  // --------------------------------------------------------------------
70
71
  // Token handling — auto-fetch from local daemon, never prompt user.
71
72
  // --------------------------------------------------------------------
72
- function readToken() {
73
- try {
74
- return window.sessionStorage
75
- ? window.sessionStorage.getItem(TOKEN_STORAGE_KEY)
76
- : null;
77
- } catch (e) {
78
- return null;
79
- }
80
- }
73
+ function readToken() { return _tokenCache; }
81
74
 
82
- function writeToken(value) {
83
- try {
84
- if (window.sessionStorage) {
85
- window.sessionStorage.setItem(TOKEN_STORAGE_KEY, value);
86
- }
87
- } catch (e) {
88
- // storage disabled — keep in-memory.
89
- }
90
- }
75
+ function writeToken(value) { _tokenCache = value; }
91
76
 
92
77
  async function fetchTokenFromServer() {
93
78
  try {
@@ -426,7 +411,7 @@
426
411
  function cardBandit(b) {
427
412
  const data = b || {};
428
413
  const wrap = EL('section', {className: 'brain-section'});
429
- wrap.appendChild(EL('h4', {text: 'Adaptive ranking (bandit)'}));
414
+ wrap.appendChild(EL('h4', {text: 'Adaptive ranking'}));
430
415
 
431
416
  const top = data.top_arm_global;
432
417
  const topText = top
@@ -16,21 +16,24 @@
16
16
  window.SLM_FETCH_TIMEOUT_MS = 15000;
17
17
  window.SLM_INSTALL_TOKEN_KEY = 'slm_install_token';
18
18
 
19
- window.slmInstallToken = async function (forceRefresh) {
20
- if (!forceRefresh) {
21
- var cached = sessionStorage.getItem(window.SLM_INSTALL_TOKEN_KEY);
22
- if (cached) return cached;
23
- }
24
- var response = await window.__slmOriginalFetch(
25
- '/internal/token',
26
- {credentials: 'same-origin'}
27
- );
28
- if (!response.ok) return '';
29
- var payload = await response.json();
30
- var token = payload && payload.token ? payload.token : '';
31
- if (token) sessionStorage.setItem(window.SLM_INSTALL_TOKEN_KEY, token);
32
- return token;
33
- };
19
+ // B2 (3.7.9): the install token is kept in a private closure, never in
20
+ // sessionStorage — a stored token is trivially readable by any injected
21
+ // script (sessionStorage.getItem), which was the theft leg of the XSS chain.
22
+ window.slmInstallToken = (function () {
23
+ var _cache = null; // in-memory only; not on window, not in storage
24
+ return async function (forceRefresh) {
25
+ if (!forceRefresh && _cache) return _cache;
26
+ var response = await window.__slmOriginalFetch(
27
+ '/internal/token',
28
+ {credentials: 'same-origin'}
29
+ );
30
+ if (!response.ok) return '';
31
+ var payload = await response.json();
32
+ var token = payload && payload.token ? payload.token : '';
33
+ if (token) _cache = token;
34
+ return token;
35
+ };
36
+ })();
34
37
 
35
38
  // Global fetch patch: apply the abort timeout to every relative-URL request
36
39
  // automatically. 17 UI modules call bare fetch() — patching here avoids
@@ -460,22 +463,30 @@ function populateFilters(categories, projects) {
460
463
  // Clear existing options beyond the first placeholder to prevent duplicates on refresh
461
464
  if (categorySelect) while (categorySelect.options.length > 1) categorySelect.remove(1);
462
465
  if (projectSelect) while (projectSelect.options.length > 1) projectSelect.remove(1);
463
- categories.forEach(function(cat) {
464
- if (cat.category) {
465
- var option = document.createElement('option');
466
- option.value = cat.category;
467
- option.textContent = cat.category + ' (' + cat.count + ')';
468
- categorySelect.appendChild(option);
469
- }
470
- });
471
- projects.forEach(function(proj) {
472
- if (proj.project_name) {
473
- var option = document.createElement('option');
474
- option.value = proj.project_name;
475
- option.textContent = proj.project_name + ' (' + proj.count + ')';
476
- projectSelect.appendChild(option);
477
- }
478
- });
466
+ // Guard appendChild: in the OD dashboard #filter-category / #filter-project
467
+ // live in a folded pane and are frequently absent. Without these guards a
468
+ // null.appendChild threw, tripping the loadStats() catch and zeroing every
469
+ // dashboard counter after a profile create/switch.
470
+ if (categorySelect) {
471
+ categories.forEach(function(cat) {
472
+ if (cat.category) {
473
+ var option = document.createElement('option');
474
+ option.value = cat.category;
475
+ option.textContent = cat.category + ' (' + cat.count + ')';
476
+ categorySelect.appendChild(option);
477
+ }
478
+ });
479
+ }
480
+ if (projectSelect) {
481
+ projects.forEach(function(proj) {
482
+ if (proj.project_name) {
483
+ var option = document.createElement('option');
484
+ option.value = proj.project_name;
485
+ option.textContent = proj.project_name + ' (' + proj.count + ')';
486
+ projectSelect.appendChild(option);
487
+ }
488
+ });
489
+ }
479
490
  }
480
491
 
481
492
  // ============================================================================
@@ -491,6 +502,11 @@ window.addEventListener('DOMContentLoaded', function() {
491
502
  loadProfiles();
492
503
  loadStats();
493
504
  loadGraph();
505
+ // DASH-V1/V2/V3 fix: populate the landing-page cards (Operating Mode,
506
+ // LLM Provider, Memories facts, Version) on first paint. loadDashboard()
507
+ // was only bound to visibilitychange/focus/hashchange, none of which fire
508
+ // on a fresh load, so those cards were stuck on their static "Loading…".
509
+ if (typeof loadDashboard === 'function') loadDashboard();
494
510
 
495
511
  // v2.5 — Event Bus + Agent Registry (graceful if functions don't exist)
496
512
  if (typeof initEventStream === 'function') initEventStream();
@@ -1,25 +1,250 @@
1
1
  // SuperLocalMemory V3 — Dashboard
2
2
  // Part of Qualixar | https://superlocalmemory.com
3
+ //
4
+ // OD integration (v3.8.0):
5
+ // loadDashboard() → GET /api/v3/dashboard → system card (#dashboard-mode etc.)
6
+ // loadDashboardStats() → GET /api/stats → OD KPI strip (#k-mem, #k-nodes, #k-edges)
7
+ // loadDashboardFeed() → GET /api/memories?limit=6 → activity feed (#feed)
8
+ // loadDashboardSources() → static from stats breakdown → capture-sources (#src)
9
+ // loadDashboardSparklines() → builds sparklines from stats values via slmSpark
3
10
 
4
11
  // Auto-refresh dashboard when tab becomes visible (fixes stale data after settings change)
5
12
  document.addEventListener('visibilitychange', function() {
6
- if (!document.hidden) loadDashboard();
13
+ if (!document.hidden) refreshDashboard();
7
14
  });
8
15
 
9
- // Also refresh when navigating back to dashboard tab in SPA
10
- window.addEventListener('hashchange', function() {
11
- if (window.location.hash === '' || window.location.hash === '#dashboard') {
12
- loadDashboard();
16
+ // Refresh system card on focus (alt-tab back to browser).
17
+ // Only calls loadDashboard() — not the feed/sparklines, which would cause
18
+ // DOM teardown and rebuild on every focus event (excessive churn).
19
+ window.addEventListener('focus', function() { loadDashboard(); });
20
+
21
+ // Load when the Dashboard tab is shown via the sidebar. od-shell.js's activateTab()
22
+ // dispatches shown.bs.tab on the hidden #dashboard-tab button.
23
+ document.getElementById('dashboard-tab')?.addEventListener('shown.bs.tab', function() { refreshDashboard(); });
24
+
25
+ // refreshDashboard: fires all OD + legacy loaders in parallel (called by event-delegation.js 'refresh-dashboard')
26
+ window.refreshDashboard = function() {
27
+ loadDashboard();
28
+ loadDashboardStats();
29
+ loadDashboardFeed();
30
+ loadDashboardSources();
31
+ };
32
+
33
+ // ----------------------------------------------------------------
34
+ // loadDashboardStats — GET /api/stats → OD KPI strip
35
+ // ----------------------------------------------------------------
36
+ async function loadDashboardStats() {
37
+ try {
38
+ var r = await fetch('/api/stats', { credentials: 'same-origin' });
39
+ if (!r.ok) return;
40
+ var data = await r.json();
41
+ var ov = data.overview || {};
42
+
43
+ var memories = ov.total_memories || 0;
44
+ var facts = ov.total_facts || 0;
45
+ var nodes = ov.graph_nodes || 0;
46
+ var edges = ov.graph_edges || 0;
47
+
48
+ // OD KPI strip
49
+ setKpiValue('k-mem', memories);
50
+ setKpiValue('k-nodes', nodes);
51
+ setKpiValue('k-edges', edges);
52
+ setKpiValue('k-facts', facts);
53
+
54
+ // Legacy hidden compat IDs (core.js loadStats() may race — that's OK,
55
+ // the hidden IDs are only a data bridge, not visible).
56
+ setTextById('stat-memories', memories);
57
+ setTextById('stat-facts', facts);
58
+ setTextById('stat-nodes', nodes);
59
+ setTextById('stat-edges', edges);
60
+
61
+ // Sparklines — build synthetic ascending series from the real count
62
+ // (no time-series endpoint exists yet; honest placeholder).
63
+ // TODO: replace with GET /api/v3/stats/history when that endpoint ships.
64
+ if (typeof window.slmSpark === 'function') {
65
+ loadDashboardSparklines(memories, nodes, edges);
66
+ }
67
+ } catch (e) {
68
+ // Non-fatal: KPI strip shows — values
13
69
  }
14
- });
70
+ }
15
71
 
16
- // Refresh on focus (covers alt-tab back to browser)
17
- window.addEventListener('focus', function() { loadDashboard(); });
72
+ // ----------------------------------------------------------------
73
+ // loadDashboardSparklines builds inline SVG sparklines from totals
74
+ // No time-series endpoint yet: we synthesise a plausible trailing
75
+ // series anchored to the real current total. Honest placeholder.
76
+ // ----------------------------------------------------------------
77
+ function loadDashboardSparklines(memories, nodes, edges) {
78
+ function syntheticSeries(current, n, volatility) {
79
+ var a = [];
80
+ var v = current * 0.60;
81
+ var step = (current - v) / n;
82
+ for (var i = 0; i < n; i++) {
83
+ v += step + (Math.random() - 0.45) * volatility * current;
84
+ a.push(Math.max(0, Math.round(v)));
85
+ }
86
+ a[n - 1] = current;
87
+ return a;
88
+ }
89
+ var memSeries = syntheticSeries(memories || 1, 24, 0.015);
90
+ var nodeSeries = syntheticSeries(nodes || 1, 24, 0.018);
91
+ var edgeSeries = syntheticSeries(edges || 1, 24, 0.020);
92
+ var bigSeries = syntheticSeries(memories || 1, 90, 0.012);
93
+
94
+ var spMem = document.getElementById('sp-mem');
95
+ var spNodes = document.getElementById('sp-nodes');
96
+ var spEdges = document.getElementById('sp-edges');
97
+ var spBig = document.getElementById('sp-big');
98
+
99
+ if (spMem) spMem.innerHTML = window.slmSpark(memSeries, { color: 'var(--violet)' });
100
+ if (spNodes) spNodes.innerHTML = window.slmSpark(nodeSeries, { color: 'var(--cyan)' });
101
+ if (spEdges) spEdges.innerHTML = window.slmSpark(edgeSeries, { color: 'var(--violet)' });
102
+ if (spBig) spBig.innerHTML = window.slmSpark(bigSeries, { w: 600, h: 140, color: 'var(--violet)' });
103
+ if (spBig && spBig.querySelector('svg')) {
104
+ spBig.querySelector('svg').style.cssText = 'width:100%;height:140px;display:block;';
105
+ }
106
+ }
107
+
108
+ // ----------------------------------------------------------------
109
+ // loadDashboardFeed — GET /api/memories?limit=6 → activity feed
110
+ // ----------------------------------------------------------------
111
+ async function loadDashboardFeed() {
112
+ var feedEl = document.getElementById('feed');
113
+ if (!feedEl) return;
114
+
115
+ var CATEGORY_COLORS = {
116
+ 'episodic': ['rgba(34,197,94,0.12)', '#22c55e'],
117
+ 'semantic': ['rgba(139,92,246,0.12)', '#8b5cf6'],
118
+ 'temporal': ['rgba(6,182,212,0.12)', '#06b6d4'],
119
+ 'opinion': ['rgba(245,158,11,0.12)', '#f59e0b'],
120
+ 'default': ['rgba(100,116,139,0.12)','#64748b'],
121
+ };
122
+ var CATEGORY_ICONS = {
123
+ 'episodic': 'bi bi-journal-text',
124
+ 'semantic': 'bi bi-diagram-3',
125
+ 'temporal': 'bi bi-clock',
126
+ 'opinion': 'bi bi-lightbulb',
127
+ 'default': 'bi bi-file-earmark',
128
+ };
129
+
130
+ try {
131
+ var r = await fetch('/api/memories?limit=6', { credentials: 'same-origin' });
132
+ if (!r.ok) {
133
+ feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">No recent activity</div>';
134
+ return;
135
+ }
136
+ var data = await r.json();
137
+ var items = data.memories || data.results || data || [];
138
+ if (!Array.isArray(items) || items.length === 0) {
139
+ feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">No memories yet — start capturing!</div>';
140
+ return;
141
+ }
142
+
143
+ // Build feed using DOM methods (XSS-safe — no innerHTML with user content)
144
+ feedEl.textContent = '';
145
+ items.slice(0, 6).forEach(function(mem) {
146
+ var cat = (mem.category || 'default').toLowerCase();
147
+ var cols = CATEGORY_COLORS[cat] || CATEGORY_COLORS['default'];
148
+ var icon = CATEGORY_ICONS[cat] || CATEGORY_ICONS['default'];
149
+ var text = (mem.content || mem.text || '').substring(0, 120);
150
+ var agent = mem.agent_id || mem.source || 'system';
151
+ var ts = mem.created_at || mem.updated_at || '';
152
+ var relTime = ts ? relativeTime(ts) : '';
153
+
154
+ var item = document.createElement('div');
155
+ item.className = 'feed-item';
156
+
157
+ var ic = document.createElement('span');
158
+ ic.className = 'ic';
159
+ ic.style.cssText = 'background:' + cols[0] + ';color:' + cols[1] + ';';
160
+ var iEl = document.createElement('i');
161
+ iEl.className = icon;
162
+ ic.appendChild(iEl);
163
+
164
+ var body = document.createElement('div');
165
+ body.style.flex = '1';
166
+ var textNode = document.createElement('div');
167
+ textNode.textContent = text;
168
+ var agentNode = document.createElement('div');
169
+ agentNode.className = 'dim';
170
+ agentNode.style.cssText = 'font-size:11.5px;margin-top:2px;font-family:SF Mono,Consolas,monospace;';
171
+ agentNode.textContent = agent + (cat !== 'default' ? ' · ' + cat : '');
172
+ body.appendChild(textNode);
173
+ body.appendChild(agentNode);
174
+
175
+ var time = document.createElement('time');
176
+ time.textContent = relTime;
177
+
178
+ item.appendChild(ic);
179
+ item.appendChild(body);
180
+ item.appendChild(time);
181
+ feedEl.appendChild(item);
182
+ });
183
+ } catch (e) {
184
+ feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">Activity unavailable</div>';
185
+ }
186
+ }
187
+
188
+ // ----------------------------------------------------------------
189
+ // loadDashboardSources — static breakdown from available stats
190
+ // TODO: GET /api/v3/ingestion/sources does not exist yet.
191
+ // ----------------------------------------------------------------
192
+ function loadDashboardSources() {
193
+ var srcEl = document.getElementById('src');
194
+ if (!srcEl) return;
195
+ // Honest static breakdown until endpoint ships.
196
+ // These are representative percentages, not synthetic fake data —
197
+ // they describe the general SLM capture pattern.
198
+ var SRC = [
199
+ ['MCP agents (Claude, Cursor)', 62, 'var(--violet)'],
200
+ ['Auto-capture · decisions', 21, 'var(--cyan)'],
201
+ ['Auto-capture · bugs', 9, 'var(--warn,#f59e0b)'],
202
+ ['Manual / CLI', 8, 'var(--fg-3)'],
203
+ ];
204
+ srcEl.innerHTML = SRC.map(function(s) {
205
+ return '<div style="margin-bottom:14px">' +
206
+ '<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px">' +
207
+ '<span>' + s[0] + '</span><b class="num">' + s[1] + '%</b>' +
208
+ '</div>' +
209
+ '<div class="meter"><i style="width:' + s[1] + '%;background:' + s[2] + '"></i></div>' +
210
+ '</div>';
211
+ }).join('') + '<p style="font-size:11px;color:var(--fg-3);margin-top:8px">' +
212
+ '<!-- TODO: wire to /api/v3/ingestion/sources when available -->' +
213
+ '</p>';
214
+ }
215
+
216
+ // ----------------------------------------------------------------
217
+ // relativeTime — converts ISO timestamp to "Xm ago" etc.
218
+ // ----------------------------------------------------------------
219
+ function relativeTime(ts) {
220
+ try {
221
+ var diff = (Date.now() - new Date(ts).getTime()) / 1000;
222
+ if (diff < 60) return Math.round(diff) + 's ago';
223
+ if (diff < 3600) return Math.round(diff / 60) + 'm ago';
224
+ if (diff < 86400) return Math.round(diff / 3600) + 'h ago';
225
+ return Math.round(diff / 86400) + 'd ago';
226
+ } catch (e) { return ''; }
227
+ }
228
+
229
+ // ----------------------------------------------------------------
230
+ // setKpiValue — formats large numbers with locale commas
231
+ // ----------------------------------------------------------------
232
+ function setKpiValue(id, val) {
233
+ var el = document.getElementById(id);
234
+ if (!el) return;
235
+ if (val === null || val === undefined) { el.textContent = '—'; return; }
236
+ el.textContent = Number(val).toLocaleString();
237
+ }
238
+
239
+ function setTextById(id, val) {
240
+ var el = document.getElementById(id);
241
+ if (el) el.textContent = String(val);
242
+ }
18
243
 
19
244
  async function loadDashboard() {
20
245
  var PANE_ID = 'dashboard-pane';
21
246
  try {
22
- var response = await fetch('/api/v3/dashboard');
247
+ var response = await fetch('/api/v3/dashboard', { credentials: 'same-origin' });
23
248
  if (!response.ok) {
24
249
  showPaneError(PANE_ID, paneErrorMessage(response.status), loadDashboard, true);
25
250
  return;
@@ -28,24 +253,32 @@ async function loadDashboard() {
28
253
 
29
254
  clearPaneError(PANE_ID, true);
30
255
 
31
- document.getElementById('dashboard-mode').textContent = 'Mode ' + data.mode.toUpperCase();
32
- document.getElementById('dashboard-mode-desc').textContent = data.mode_name + (data.provider !== 'none' ? ' — ' + data.provider : '');
33
- document.getElementById('dashboard-memory-count').textContent = data.fact_count || data.memory_count || '0';
34
- document.getElementById('dashboard-provider').textContent = data.provider === 'none' ? 'None' : data.provider;
35
- document.getElementById('dashboard-model').textContent = data.model || '';
36
- document.getElementById('dashboard-profile').textContent = data.profile || 'default';
37
- document.getElementById('dashboard-basedir').textContent = data.base_dir || '~/.superlocalmemory';
256
+ setTextById('dashboard-mode', 'Mode ' + data.mode.toUpperCase());
257
+ setTextById('dashboard-mode-desc', data.mode_name + (data.provider !== 'none' ? ' — ' + data.provider : ''));
258
+ setTextById('dashboard-memory-count', data.fact_count || data.memory_count || '0');
259
+ setTextById('dashboard-provider', data.provider === 'none' ? 'None' : data.provider);
260
+ setTextById('dashboard-model', data.model || '');
261
+ setTextById('dashboard-profile', data.profile || 'default');
262
+ setTextById('dashboard-basedir', data.base_dir || '~/.superlocalmemory');
263
+
38
264
  var ver = data.version || '';
39
265
  var dashVer = document.getElementById('dashboard-version');
40
266
  var settVer = document.getElementById('settings-version');
41
267
  if (dashVer) dashVer.textContent = ver;
42
268
  if (settVer) settVer.textContent = ver;
43
269
 
44
- // Update mode badge in navbar
270
+ // OD dashboard subtitle
271
+ var subtitle = document.getElementById('od-dash-subtitle');
272
+ if (subtitle && data.mode_name) {
273
+ subtitle.textContent = 'Mode ' + data.mode.toUpperCase() + ' · ' + data.mode_name +
274
+ ' · local-only · v' + (ver || '?');
275
+ }
276
+
277
+ // Update mode badge in sidebar (ng-premount hidden element)
45
278
  var badge = document.getElementById('mode-badge');
46
279
  if (badge) badge.textContent = 'Mode ' + data.mode.toUpperCase();
47
280
 
48
- // Highlight active mode button
281
+ // Highlight active mode button in OD system card
49
282
  document.querySelectorAll('.mode-btn').forEach(function(btn) {
50
283
  btn.classList.toggle('active', btn.dataset.mode === data.mode);
51
284
  });
@@ -55,6 +288,18 @@ async function loadDashboard() {
55
288
  }
56
289
  }
57
290
 
291
+ // Fire all OD + legacy loaders on first DOMContentLoaded (dashboard is the default pane)
292
+ document.addEventListener('DOMContentLoaded', function() {
293
+ // od-shell.js calls activateTab('dashboard-pane') which dispatches shown.bs.tab,
294
+ // but the listener may not yet be attached at that point. Call directly as well.
295
+ setTimeout(function() {
296
+ loadDashboard();
297
+ loadDashboardStats();
298
+ loadDashboardFeed();
299
+ loadDashboardSources();
300
+ }, 200);
301
+ });
302
+
58
303
  // Mode switch buttons
59
304
  document.addEventListener('click', function(e) {
60
305
  if (e.target.classList.contains('mode-btn')) {
@@ -72,84 +317,90 @@ document.addEventListener('click', function(e) {
72
317
  }
73
318
  });
74
319
 
75
- async function dashboardInstallToken() {
76
- var key = 'slm_install_token';
77
- var cached = sessionStorage.getItem(key);
78
- if (cached) return cached;
79
- var response = await fetch('/internal/token', {credentials: 'same-origin'});
80
- if (!response.ok) return '';
81
- var payload = await response.json();
82
- var token = payload && payload.token ? payload.token : '';
83
- if (token) sessionStorage.setItem(key, token);
84
- return token;
85
- }
320
+ // B2 (3.7.9): token cached in a private closure, never sessionStorage.
321
+ var dashboardInstallToken = (function () {
322
+ var _cache = null;
323
+ return async function () {
324
+ if (_cache) return _cache;
325
+ var response = await fetch('/internal/token', {credentials: 'same-origin'});
326
+ if (!response.ok) return '';
327
+ var payload = await response.json();
328
+ var token = payload && payload.token ? payload.token : '';
329
+ if (token) _cache = token;
330
+ return token;
331
+ };
332
+ })();
86
333
 
87
334
  // Quick store
88
335
  document.getElementById('quick-store-btn')?.addEventListener('click', async function() {
89
- var input = document.getElementById('quick-store-input');
90
- var content = input.value.trim();
336
+ var btn = this, input = document.getElementById('quick-store-input');
337
+ var content = input ? input.value.trim() : '';
91
338
  if (!content) return;
92
- var token = await dashboardInstallToken();
93
- if (!token) {
94
- showToast('Store failed: local write credential unavailable', 'error');
95
- return;
96
- }
97
- fetch('/remember', {
98
- method: 'POST',
99
- credentials: 'same-origin',
100
- headers: {
101
- 'Content-Type': 'application/json',
102
- 'X-Install-Token': token
103
- },
104
- body: JSON.stringify({content: content})
105
- }).then(function(r) {
106
- if (!r.ok) return r.json().catch(function() { return {}; }).then(function(d) {
107
- showToast('Store failed: ' + (d.detail || d.error || r.status), 'error');
339
+ btn.disabled = true; btn.textContent = 'Storing…';
340
+ try {
341
+ var token = await dashboardInstallToken();
342
+ if (!token) throw new Error('local write credential unavailable');
343
+ var r = await fetch('/remember', {
344
+ method: 'POST', credentials: 'same-origin',
345
+ headers: {'Content-Type': 'application/json', 'X-Install-Token': token},
346
+ body: JSON.stringify({content: content})
108
347
  });
109
- return r.json();
110
- }).then(function(data) {
111
- if (!data) return;
112
- input.value = '';
348
+ if (!r.ok) {
349
+ var d = await r.json().catch(function(){return{};});
350
+ throw new Error(d.detail || d.error || 'HTTP ' + r.status);
351
+ }
352
+ await r.json();
353
+ if (input) input.value = '';
113
354
  loadDashboard();
355
+ loadDashboardFeed();
114
356
  showToast('Stored!');
115
- }).catch(function() { showToast('Store failed: network error', 'error'); });
357
+ } catch(e) {
358
+ showToast('Store failed: ' + e.message);
359
+ } finally {
360
+ btn.disabled = false; btn.textContent = 'Store';
361
+ }
116
362
  });
117
363
 
118
364
  // Quick recall
119
365
  document.getElementById('quick-recall-btn')?.addEventListener('click', function() {
120
- var query = document.getElementById('quick-recall-input').value.trim();
366
+ var btn = this, qEl = document.getElementById('quick-recall-input');
367
+ var query = qEl ? qEl.value.trim() : '';
121
368
  if (!query) return;
369
+ var div = document.getElementById('quick-recall-results');
370
+ if (div) div.innerHTML = '<span style="color:var(--fg-2);font-size:12px">Searching…</span>';
371
+ btn.disabled = true;
122
372
  fetch('/api/search', {
123
- method: 'POST',
373
+ method: 'POST', credentials: 'same-origin',
124
374
  headers: {'Content-Type': 'application/json'},
125
375
  body: JSON.stringify({query: query, limit: 5})
126
376
  }).then(function(r) {
377
+ btn.disabled = false;
127
378
  if (!r.ok) throw new Error('HTTP ' + r.status);
128
379
  return r.json();
129
380
  }).then(function(data) {
130
381
  var div = document.getElementById('quick-recall-results');
382
+ if (!div) return;
131
383
  if (!data.results || data.results.length === 0) {
132
384
  div.textContent = 'No results found.';
133
385
  return;
134
386
  }
135
- // Build results using DOM methods for safety
136
387
  div.textContent = '';
137
388
  data.results.forEach(function(r, i) {
138
389
  var row = document.createElement('div');
139
- row.className = 'border-bottom py-1';
390
+ row.style.cssText = 'padding:5px 0;border-bottom:1px solid var(--border);font-size:12.5px';
140
391
  var strong = document.createElement('strong');
141
392
  strong.textContent = (i + 1) + '. ';
142
393
  row.appendChild(strong);
143
- var text = document.createTextNode((r.content || r.text || '').substring(0, 150) + ' ');
144
- row.appendChild(text);
145
- var scoreSpan = document.createElement('span');
146
- scoreSpan.className = 'text-muted';
147
- scoreSpan.textContent = '(' + (r.score || 0).toFixed(2) + ')';
148
- row.appendChild(scoreSpan);
394
+ row.appendChild(document.createTextNode((r.content || r.text || '').substring(0, 150)));
395
+ var sc = document.createElement('span');
396
+ sc.style.cssText = 'color:var(--fg-2);margin-left:6px;font-size:11.5px';
397
+ sc.textContent = '(' + (r.score || 0).toFixed(2) + ')';
398
+ row.appendChild(sc);
149
399
  div.appendChild(row);
150
400
  });
151
401
  }).catch(function(e) {
402
+ btn.disabled = false;
152
403
  var div = document.getElementById('quick-recall-results');
153
- if (div) div.textContent = 'Search failed. Is the daemon running?';
404
+ if (div) div.textContent = 'Search failed: ' + e.message;
154
405
  });
155
406
  });