superlocalmemory 3.7.8 → 3.8.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 (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +129 -0
  4. package/README.md +205 -123
  5. package/package.json +12 -3
  6. package/plugin/.claude-plugin/plugin.json +2 -3
  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/rules/AGENTS.md +57 -18
  30. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  31. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  32. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  33. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  34. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  36. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  37. package/pyproject.toml +2 -1
  38. package/scripts/postinstall/validation.js +2 -0
  39. package/scripts/postinstall-interactive.js +74 -2
  40. package/src/superlocalmemory/__init__.py +1 -1
  41. package/src/superlocalmemory/access/__init__.py +3 -0
  42. package/src/superlocalmemory/access/rbac.py +477 -0
  43. package/src/superlocalmemory/cli/commands.py +228 -17
  44. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  45. package/src/superlocalmemory/cli/daemon.py +7 -0
  46. package/src/superlocalmemory/cli/loop_cmd.py +187 -0
  47. package/src/superlocalmemory/cli/main.py +49 -8
  48. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  49. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  50. package/src/superlocalmemory/cli/pending_store.py +49 -13
  51. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  52. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  53. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  54. package/src/superlocalmemory/cli/version_banner.py +17 -3
  55. package/src/superlocalmemory/compliance/audit.py +6 -0
  56. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  57. package/src/superlocalmemory/compliance/retention.py +176 -45
  58. package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
  59. package/src/superlocalmemory/core/community_summary.py +267 -0
  60. package/src/superlocalmemory/core/config.py +216 -3
  61. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  62. package/src/superlocalmemory/core/context_cache.py +61 -18
  63. package/src/superlocalmemory/core/embedding_worker.py +21 -7
  64. package/src/superlocalmemory/core/embeddings.py +131 -46
  65. package/src/superlocalmemory/core/engine.py +41 -22
  66. package/src/superlocalmemory/core/engine_ingestion.py +359 -43
  67. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  68. package/src/superlocalmemory/core/entity_community.py +178 -0
  69. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  70. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  71. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  72. package/src/superlocalmemory/core/injection.py +12 -7
  73. package/src/superlocalmemory/core/key_expander.py +138 -0
  74. package/src/superlocalmemory/core/maintenance.py +23 -0
  75. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  76. package/src/superlocalmemory/core/modes.py +1 -1
  77. package/src/superlocalmemory/core/mutations.py +2 -2
  78. package/src/superlocalmemory/core/pii.py +105 -0
  79. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  80. package/src/superlocalmemory/core/recall_pipeline.py +7 -3
  81. package/src/superlocalmemory/core/recall_worker.py +20 -6
  82. package/src/superlocalmemory/core/scale_engine.py +60 -1
  83. package/src/superlocalmemory/core/security_primitives.py +40 -2
  84. package/src/superlocalmemory/core/store_pipeline.py +186 -29
  85. package/src/superlocalmemory/core/worker_pool.py +21 -6
  86. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  87. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  88. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  89. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  90. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  91. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  92. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  93. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  94. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  95. package/src/superlocalmemory/evolution/triggers.py +22 -13
  96. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  97. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  98. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  100. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  101. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  102. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  103. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  104. package/src/superlocalmemory/hooks/portable_kit.py +148 -3
  105. package/src/superlocalmemory/infra/backup.py +12 -1
  106. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  107. package/src/superlocalmemory/infra/data_root.py +43 -4
  108. package/src/superlocalmemory/infra/event_bus.py +107 -24
  109. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  110. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  111. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  112. package/src/superlocalmemory/learning/cross_project.py +28 -19
  113. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  114. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  115. package/src/superlocalmemory/learning/reward.py +50 -0
  116. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  117. package/src/superlocalmemory/learning/source_quality.py +523 -1
  118. package/src/superlocalmemory/loops/__init__.py +56 -0
  119. package/src/superlocalmemory/loops/budget.py +58 -0
  120. package/src/superlocalmemory/loops/engine.py +164 -0
  121. package/src/superlocalmemory/loops/ledger.py +263 -0
  122. package/src/superlocalmemory/loops/models.py +152 -0
  123. package/src/superlocalmemory/loops/rules.py +52 -0
  124. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  125. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  126. package/src/superlocalmemory/mcp/profiles.py +103 -0
  127. package/src/superlocalmemory/mcp/server.py +32 -79
  128. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  129. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  130. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  131. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  132. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  133. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  134. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  135. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  136. package/src/superlocalmemory/mesh/broker.py +237 -129
  137. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  138. package/src/superlocalmemory/optimize/NOTICE +1 -6
  139. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  140. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  141. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  142. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  143. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  144. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  145. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  146. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  147. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  148. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  149. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  150. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  151. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  152. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  153. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  154. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  155. package/src/superlocalmemory/retrieval/engine.py +221 -47
  156. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  157. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  158. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  159. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  160. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  161. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  162. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  163. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  164. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  165. package/src/superlocalmemory/server/api.py +4 -4
  166. package/src/superlocalmemory/server/config_file.py +90 -0
  167. package/src/superlocalmemory/server/origin.py +50 -0
  168. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  169. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  170. package/src/superlocalmemory/server/recall_health.py +24 -3
  171. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  172. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  173. package/src/superlocalmemory/server/routes/agents.py +128 -38
  174. package/src/superlocalmemory/server/routes/backup.py +317 -70
  175. package/src/superlocalmemory/server/routes/behavioral.py +349 -71
  176. package/src/superlocalmemory/server/routes/brain.py +69 -12
  177. package/src/superlocalmemory/server/routes/chat.py +10 -5
  178. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  179. package/src/superlocalmemory/server/routes/config_api.py +438 -0
  180. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  181. package/src/superlocalmemory/server/routes/entity.py +108 -26
  182. package/src/superlocalmemory/server/routes/events.py +24 -8
  183. package/src/superlocalmemory/server/routes/evolution.py +189 -68
  184. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  185. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  186. package/src/superlocalmemory/server/routes/insights.py +3 -3
  187. package/src/superlocalmemory/server/routes/learning.py +289 -118
  188. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  189. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  190. package/src/superlocalmemory/server/routes/memories.py +182 -57
  191. package/src/superlocalmemory/server/routes/mesh.py +200 -31
  192. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  193. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  194. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  195. package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
  196. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  197. package/src/superlocalmemory/server/routes/stats.py +103 -158
  198. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  199. package/src/superlocalmemory/server/routes/token.py +3 -13
  200. package/src/superlocalmemory/server/routes/v3_api.py +247 -89
  201. package/src/superlocalmemory/server/routes/ws.py +5 -2
  202. package/src/superlocalmemory/server/security_middleware.py +12 -5
  203. package/src/superlocalmemory/server/ui.py +20 -5
  204. package/src/superlocalmemory/server/unified_daemon.py +827 -72
  205. package/src/superlocalmemory/server/write_identity.py +38 -8
  206. package/src/superlocalmemory/storage/database.py +265 -53
  207. package/src/superlocalmemory/storage/migration_runner.py +132 -1
  208. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -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/migrations/M028_fact_entity_associations.py +270 -0
  217. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  218. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  219. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  220. package/src/superlocalmemory/storage/models.py +4 -0
  221. package/src/superlocalmemory/storage/schema.py +136 -1
  222. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  223. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  224. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  225. package/src/superlocalmemory/trust/gate.py +49 -8
  226. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  227. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  228. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  229. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  230. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  231. package/src/superlocalmemory/ui/favicon.svg +35 -4
  232. package/src/superlocalmemory/ui/index.html +303 -173
  233. package/src/superlocalmemory/ui/js/brain.js +5 -20
  234. package/src/superlocalmemory/ui/js/core.js +100 -41
  235. package/src/superlocalmemory/ui/js/dashboard.js +403 -65
  236. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  237. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  238. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  239. package/src/superlocalmemory/ui/js/memories.js +15 -4
  240. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  241. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  242. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  243. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  244. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  245. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  246. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  247. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  248. package/src/superlocalmemory/ui/js/od-backup.js +871 -0
  249. package/src/superlocalmemory/ui/js/od-brain.js +816 -0
  250. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  251. package/src/superlocalmemory/ui/js/od-graph.js +600 -0
  252. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  253. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  254. package/src/superlocalmemory/ui/js/od-memories.js +929 -0
  255. package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
  256. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  257. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  258. package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
  259. package/src/superlocalmemory/ui/js/od-shell.js +809 -0
  260. package/src/superlocalmemory/ui/js/od-skills.js +600 -0
  261. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  262. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  263. package/src/superlocalmemory/ui/js/settings.js +17 -3
  264. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  265. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  266. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  267. package/plugin-src/.mcp.json +0 -12
  268. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  269. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  270. package/plugin-src/hooks/.gitkeep +0 -0
  271. package/plugin-src/hooks/hooks.json +0 -23
  272. package/plugin-src/manifest.json +0 -25
  273. package/plugin-src/requirements.txt +0 -1
  274. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  275. package/plugin-src/scripts/ensure-venv.bat +0 -122
  276. package/plugin-src/scripts/ensure-venv.sh +0 -105
  277. package/plugin-src/scripts/slm-launch +0 -23
  278. package/plugin-src/scripts/slm-launch.bat +0 -23
  279. package/plugin-src/settings.json +0 -16
  280. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -0,0 +1,553 @@
1
+ // SuperLocalMemory — od-mesh.js v2.0
2
+ // OD-styled Mesh Peers pane. Self-contained, CSP-safe.
3
+ // Renders into the "mesh-pane" tab container via the shell's triggerTabLoad.
4
+ // Exposes window.odRenderMesh(container).
5
+ //
6
+ // HTML structure mirrors approved design (mesh-peers.html <main class="content">).
7
+ // Design-system CSS classes used throughout — no invented inline colours.
8
+ //
9
+ // Endpoints (all via meshFetch → X-Install-Token):
10
+ // GET /mesh/status → {broker_up, remote_peer_count, local_session_count, uptime_s}
11
+ // GET /mesh/peers?view=all → {peers:[{peer_id, session_id, status, last_heartbeat,
12
+ // summary, host, port, agent_type, project_path}]}
13
+ // POST /mesh/send → body:{from_peer, to_peer, content, type} → {ok:true}
14
+ //
15
+ // JSON keys mapped:
16
+ // status : broker_up(bool), peer_count(int), uptime_s(int)
17
+ // peer : peer_id, session_id, summary, status, host, port,
18
+ // agent_type, last_heartbeat, project_path
19
+ // send OK : ok(bool) | send ERR: detail(str)
20
+ //
21
+ // CRIT fixes baked in:
22
+ // C1 — send guarded: button disabled when no peer selected or peer is not active
23
+ // C2 — fetch errors surface in conversation log; no silent broken state
24
+ // C3 — all API strings through escapeHtml() before innerHTML assignment
25
+ //
26
+ // Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
27
+
28
+ (function () {
29
+ 'use strict';
30
+
31
+ var REFRESH_MS = 5000;
32
+ var SEND_FROM = 'dashboard';
33
+
34
+ // ── Token helpers (mirrors ng-mesh.js; never stored in sessionStorage) ────
35
+ var _tokenCache = null;
36
+
37
+ function fetchTokenFromServer() {
38
+ return fetch('/internal/token', { credentials: 'same-origin' })
39
+ .then(function (r) { return r.ok ? r.json() : null; })
40
+ .then(function (d) {
41
+ var tok = d && typeof d.token === 'string' ? d.token.trim() : '';
42
+ if (tok) _tokenCache = tok;
43
+ return tok || null;
44
+ })
45
+ .catch(function () { return null; });
46
+ }
47
+
48
+ function ensureToken() {
49
+ return _tokenCache ? Promise.resolve(_tokenCache) : fetchTokenFromServer();
50
+ }
51
+
52
+ // Authenticated fetch; retries once on 401 (stale token).
53
+ function meshFetch(url, opts) {
54
+ return ensureToken().then(function (token) {
55
+ var base = Object.assign({ credentials: 'same-origin' }, opts || {});
56
+ if (token) {
57
+ base.headers = Object.assign({ 'X-Install-Token': token }, base.headers || {});
58
+ }
59
+ return fetch(url, base).then(function (r) {
60
+ if (r.status !== 401) return r;
61
+ return fetchTokenFromServer().then(function (fresh) {
62
+ var retry = Object.assign({ credentials: 'same-origin' }, opts || {});
63
+ if (fresh) {
64
+ retry.headers = Object.assign({ 'X-Install-Token': fresh }, retry.headers || {});
65
+ }
66
+ return fetch(url, retry);
67
+ });
68
+ });
69
+ });
70
+ }
71
+
72
+ // ── Module state ──────────────────────────────────────────────────────────
73
+ var _peers = []; // last-fetched peer list
74
+ var _selectedId = null; // peer_id currently shown in conversation panel
75
+ var _convLogs = {}; // peer_id → [{role:'q'|'a', text}]
76
+ var _container = null;
77
+ var _timer = null;
78
+ var _observer = null; // MutationObserver — re-starts refresh on tab activate
79
+ var _requestSeq = 0;
80
+
81
+ // ── HTML helpers ──────────────────────────────────────────────────────────
82
+ // CRIT C3: every API-derived string MUST pass through this.
83
+ function escapeHtml(s) {
84
+ return String(s == null ? '' : s)
85
+ .replace(/&/g, '&amp;')
86
+ .replace(/</g, '&lt;')
87
+ .replace(/>/g, '&gt;')
88
+ .replace(/"/g, '&quot;')
89
+ .replace(/'/g, '&#39;');
90
+ }
91
+
92
+ function badgeCls(st) {
93
+ return st === 'active' ? 'ok' : st === 'stale' ? 'warn' : 'danger';
94
+ }
95
+
96
+ function timeAgo(iso) {
97
+ if (!iso) return 'unknown';
98
+ var d = (Date.now() - new Date(iso).getTime()) / 1000;
99
+ if (d < 5) return 'now';
100
+ if (d < 60) return Math.floor(d) + 's';
101
+ if (d < 3600) return Math.floor(d / 60) + 'm';
102
+ if (d < 86400) return Math.floor(d / 3600) + 'h';
103
+ return Math.floor(d / 86400) + 'd';
104
+ }
105
+
106
+ function fmtUptime(sec) {
107
+ if (!sec) return '—';
108
+ var h = Math.floor(sec / 3600);
109
+ if (h > 24) return Math.floor(h / 24) + 'd';
110
+ var m = Math.floor((sec % 3600) / 60);
111
+ return h > 0 ? h + 'h ' + m + 'm' : m + 'm';
112
+ }
113
+
114
+ function sendSvg() {
115
+ // Use shell's slmIcon if available; inline fallback for safety.
116
+ if (typeof window.slmIcon === 'function') return window.slmIcon('send');
117
+ return '<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor"' +
118
+ ' stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
119
+ '<line x1="22" y1="2" x2="11" y2="13"/>' +
120
+ '<polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>';
121
+ }
122
+
123
+ // ── Layout HTML ───────────────────────────────────────────────────────────
124
+ // Mirrors <main class="content"> from approved design (mesh-peers.html).
125
+ function buildHTML() {
126
+ return (
127
+ '<div id="od-mesh-root">' +
128
+
129
+ // Page head — description updated live from /mesh/status
130
+ '<div class="page-head">' +
131
+ '<h2>Mesh peers</h2>' +
132
+ '<p id="od-mesh-page-desc">Loading mesh status…</p>' +
133
+ '</div>' +
134
+
135
+ // Two-column: peer cards (left) + conversation panel (right)
136
+ '<div style="display:grid;grid-template-columns:1.15fr 1fr;gap:20px;align-items:start">' +
137
+
138
+ // LEFT — peer card grid
139
+ '<div>' +
140
+ '<div class="launch-grid" id="od-mesh-peers-grid"' +
141
+ ' style="grid-template-columns:1fr 1fr">' +
142
+ '<div id="od-mesh-peers-ph"' +
143
+ ' style="grid-column:1/-1;text-align:center;padding:40px;color:var(--fg-2)">' +
144
+ '<div style="font-size:15px;margin-bottom:6px">Loading peers…</div>' +
145
+ '</div>' +
146
+ '</div>' +
147
+ '</div>' +
148
+
149
+ // RIGHT — conversation panel (sticky, full-height card)
150
+ '<div class="card"' +
151
+ ' style="position:sticky;top:86px;display:flex;flex-direction:column;' +
152
+ 'height:calc(100vh - 120px)">' +
153
+
154
+ // Card header: selected peer info
155
+ '<div class="card-head">' +
156
+ '<span class="avatar" id="od-mesh-cv-avatar"' +
157
+ ' style="width:30px;height:30px">?</span>' +
158
+ '<div>' +
159
+ '<h3 id="od-mesh-cv-name">No peer selected</h3>' +
160
+ '<span class="sub" id="od-mesh-cv-meta">' +
161
+ 'Click a peer card to start messaging' +
162
+ '</span>' +
163
+ '</div>' +
164
+ '<div class="spacer"></div>' +
165
+ '<span class="badge neutral" id="od-mesh-cv-badge">—</span>' +
166
+ '</div>' +
167
+
168
+ // Conversation log
169
+ '<div class="ask-log" id="od-mesh-cv-log"' +
170
+ ' style="flex:1;max-height:none;padding:16px;display:flex;flex-direction:column;gap:10px">' +
171
+ '<div style="text-align:center;color:var(--fg-3);font-size:13px;margin-top:30px">' +
172
+ 'Select a peer to start messaging' +
173
+ '</div>' +
174
+ '</div>' +
175
+
176
+ // Input row — C1: disabled until active peer selected
177
+ '<div class="ask-input" style="border-top:1px solid var(--border)">' +
178
+ '<input id="od-mesh-cv-input" disabled autocomplete="off"' +
179
+ ' placeholder="Select a peer to start messaging">' +
180
+ '<button id="od-mesh-cv-send" disabled aria-label="Send message">' +
181
+ sendSvg() +
182
+ '</button>' +
183
+ '</div>' +
184
+
185
+ '</div>' + // end conversation panel
186
+
187
+ '</div>' + // end two-column
188
+ '</div>' // od-mesh-root
189
+ );
190
+ }
191
+
192
+ // ── Render: page-head description from /mesh/status ───────────────────────
193
+ function renderPageDesc(data) {
194
+ var el = document.getElementById('od-mesh-page-desc');
195
+ if (!el) return;
196
+ if (!data) {
197
+ el.textContent = 'Agents sharing this memory mesh on your machine and LAN.' +
198
+ ' Peers go stale after 5 min without a heartbeat, dead after 30.';
199
+ return;
200
+ }
201
+ var up = data.broker_up === true ? 'Broker up' : 'Broker offline';
202
+ var remoteCount = data.remote_peer_count != null ? data.remote_peer_count : data.peer_count;
203
+ var cnt = remoteCount != null
204
+ ? remoteCount + ' remote peer' + (remoteCount === 1 ? '' : 's')
205
+ : '';
206
+ var localCount = data.local_session_count || 0;
207
+ var upt = data.uptime_s != null ? 'uptime ' + fmtUptime(data.uptime_s) : '';
208
+ var parts = [up];
209
+ if (cnt) parts.push(cnt);
210
+ if (localCount) parts.push(localCount + ' local session' + (localCount === 1 ? '' : 's'));
211
+ if (upt) parts.push(upt);
212
+ el.textContent = 'Agents sharing this memory mesh on your machine and LAN. ' +
213
+ parts.join(' · ') + '. Peers go stale after 5 min without a heartbeat, dead after 30.';
214
+ }
215
+
216
+ // ── Render: peer cards ────────────────────────────────────────────────────
217
+ function meshEmptyIcon() {
218
+ if (typeof window.slmIcon === 'function') return window.slmIcon('mesh');
219
+ return '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor"' +
220
+ ' stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
221
+ '<circle cx="12" cy="12" r="2.5"/><circle cx="5" cy="5" r="2"/>' +
222
+ '<circle cx="19" cy="5" r="2"/><circle cx="5" cy="19" r="2"/>' +
223
+ '<circle cx="19" cy="19" r="2"/>' +
224
+ '<path d="M10 10L6.5 6.5M14 10l3.5-3.5M10 14l-3.5 3.5M14 14l3.5 3.5"/></svg>';
225
+ }
226
+
227
+ function renderPeers(peers) {
228
+ var grid = document.getElementById('od-mesh-peers-grid');
229
+ if (!grid) return;
230
+
231
+ if (!peers || peers.length === 0) {
232
+ // Design pattern: proper launch-card in the grid, not bare centered text
233
+ grid.innerHTML =
234
+ '<div class="card launch-card"' +
235
+ ' style="grid-column:1/-1;display:flex;flex-direction:column;' +
236
+ 'align-items:center;justify-content:center;' +
237
+ 'padding:48px 24px;text-align:center;cursor:default">' +
238
+ '<div style="width:44px;height:44px;border-radius:12px;' +
239
+ 'background:var(--card-2);display:flex;align-items:center;' +
240
+ 'justify-content:center;margin-bottom:16px;color:var(--fg-3)">' +
241
+ meshEmptyIcon() +
242
+ '</div>' +
243
+ '<h3 style="font-size:15px;margin-bottom:6px">No mesh sessions connected</h3>' +
244
+ '<p style="font-size:12.5px;color:var(--fg-2);' +
245
+ 'max-width:30ch;line-height:1.5">' +
246
+ 'Start another agent session or connect a trusted LAN peer. Sessions register via the' +
247
+ ' <code>mesh_summary</code> MCP tool.' +
248
+ '</p>' +
249
+ '</div>';
250
+ return;
251
+ }
252
+
253
+ var html = '';
254
+ peers.forEach(function (p) {
255
+ var st = p.status || 'unknown';
256
+ var name = escapeHtml(p.session_id || p.peer_id || 'Unknown');
257
+ var pidFrag = escapeHtml((p.peer_id || '').slice(0, 12));
258
+ var summary = escapeHtml(p.summary || 'No summary');
259
+ var atype = escapeHtml(p.agent_type || 'unknown');
260
+ var isLocal = String(p.host || '').toLowerCase() === '127.0.0.1' ||
261
+ String(p.host || '').toLowerCase() === 'localhost' || String(p.host || '') === '::1';
262
+ var scopeLabel = isLocal ? 'local session' : 'remote peer';
263
+ var ago = escapeHtml(timeAgo(p.last_heartbeat));
264
+ var proj = escapeHtml(p.project_path || '~');
265
+ var port = p.port ? ' · :' + p.port : '';
266
+ var letter = (p.session_id || p.peer_id || '?').charAt(0).toUpperCase();
267
+ var avatarBg = p.agent_type === 'mcp' ? 'var(--violet)' : 'var(--cyan)';
268
+ var pid = escapeHtml(p.peer_id || '');
269
+ var toolCnt = Number(p.tool_count || 0);
270
+ var memCnt = Number(p.memory_count || 0);
271
+ // Latency — API may return latency_ms; dead peers always show '—'
272
+ var latencyStr = p.latency_ms != null
273
+ ? escapeHtml(String(p.latency_ms)) + ' ms'
274
+ : (st === 'dead' ? '—' : '—');
275
+
276
+ html +=
277
+ '<div class="card launch-card" data-peer-id="' + pid + '"' +
278
+ ' role="button" tabindex="0" aria-label="Select peer ' + name + '">' +
279
+ '<div style="display:flex;align-items:center;gap:11px;margin-bottom:12px">' +
280
+ '<span class="avatar" style="background:' + avatarBg + '">' + letter + '</span>' +
281
+ '<div style="flex:1;min-width:0">' +
282
+ '<h3 style="font-size:15px;white-space:nowrap;overflow:hidden;' +
283
+ 'text-overflow:ellipsis">' + name + '</h3>' +
284
+ '<span class="mono dim" style="font-size:11px">' + pidFrag + ' · ' + scopeLabel + '</span>' +
285
+ '</div>' +
286
+ '<span class="badge ' + badgeCls(st) + '">' +
287
+ '<span class="dot"></span>' + escapeHtml(st) +
288
+ '</span>' +
289
+ '</div>' +
290
+ '<p style="font-size:12.5px;color:var(--fg-2);margin-bottom:12px;' +
291
+ 'overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;' +
292
+ '-webkit-box-orient:vertical">' + summary + '</p>' +
293
+ // Design: proto / latency / last (three items)
294
+ '<div style="display:flex;gap:14px;flex-wrap:wrap;font-size:11.5px" class="mono">' +
295
+ '<span><span class="dim">proto</span> ' + atype + '</span>' +
296
+ '<span><span class="dim">memories</span> ' + memCnt + '</span>' +
297
+ '<span><span class="dim">tools</span> ' + toolCnt + '</span>' +
298
+ '<span><span class="dim">last</span> ' + ago + '</span>' +
299
+ '</div>' +
300
+ '<div class="mono dim" style="font-size:11px;margin-top:8px">' +
301
+ proj + port +
302
+ '</div>' +
303
+ '</div>';
304
+ });
305
+ grid.innerHTML = html;
306
+ }
307
+
308
+ // ── Peer selection: update conversation panel header + input state ─────────
309
+ function selectPeerById(pid) {
310
+ _selectedId = pid || null;
311
+ var peer = pid ? _peers.find(function (p) { return p.peer_id === pid; }) : null;
312
+
313
+ var avatarEl = document.getElementById('od-mesh-cv-avatar');
314
+ var nameEl = document.getElementById('od-mesh-cv-name');
315
+ var metaEl = document.getElementById('od-mesh-cv-meta');
316
+ var badgeEl = document.getElementById('od-mesh-cv-badge');
317
+ var inputEl = document.getElementById('od-mesh-cv-input');
318
+ var sendEl = document.getElementById('od-mesh-cv-send');
319
+
320
+ if (!peer) {
321
+ if (avatarEl) { avatarEl.textContent = '?'; avatarEl.style.background = ''; }
322
+ if (nameEl) nameEl.textContent = 'No peer selected';
323
+ if (metaEl) metaEl.textContent = 'Click a peer card to start messaging';
324
+ if (badgeEl) { badgeEl.className = 'badge neutral'; badgeEl.textContent = '—'; }
325
+ // CRIT C1: disable when no peer
326
+ if (inputEl) { inputEl.disabled = true; inputEl.placeholder = 'Select a peer to start messaging'; }
327
+ if (sendEl) { sendEl.disabled = true; sendEl.style.opacity = '0.4'; }
328
+ renderConv();
329
+ return;
330
+ }
331
+
332
+ var st = peer.status || 'unknown';
333
+ // CRIT C1: dead peers are gone from the broker — disable send.
334
+ // Stale peers are still registered and can queue messages (matches design behaviour).
335
+ var canSend = (st !== 'dead');
336
+ var atype = peer.agent_type || 'unknown';
337
+ var letter = (peer.session_id || peer.peer_id || '?').charAt(0).toUpperCase();
338
+ var avatarBg = atype === 'mcp' ? 'var(--violet)' : 'var(--cyan)';
339
+
340
+ if (avatarEl) { avatarEl.textContent = letter; avatarEl.style.background = avatarBg; }
341
+ if (nameEl) nameEl.textContent = peer.session_id || peer.peer_id || 'Unknown';
342
+ if (metaEl) metaEl.textContent =
343
+ atype + ' · ' + st +
344
+ (peer.host && peer.port ? ' · ' + peer.host + ':' + peer.port : '');
345
+ if (badgeEl) {
346
+ badgeEl.className = 'badge ' + badgeCls(st);
347
+ badgeEl.innerHTML = '<span class="dot"></span> ' + escapeHtml(st);
348
+ }
349
+ if (inputEl) {
350
+ inputEl.disabled = !canSend;
351
+ inputEl.placeholder = st === 'dead'
352
+ ? 'Peer is unreachable'
353
+ : 'Message ' + escapeHtml(peer.session_id || 'peer') + '…';
354
+ }
355
+ if (sendEl) {
356
+ sendEl.disabled = !canSend;
357
+ sendEl.style.opacity = canSend ? '1' : '0.4';
358
+ }
359
+ renderConv();
360
+ }
361
+
362
+ // ── Render: conversation log ──────────────────────────────────────────────
363
+ function renderConv() {
364
+ var log = document.getElementById('od-mesh-cv-log');
365
+ if (!log) return;
366
+ var msgs = _selectedId ? (_convLogs[_selectedId] || []) : [];
367
+ if (msgs.length === 0) {
368
+ log.innerHTML =
369
+ '<div style="text-align:center;color:var(--fg-3);font-size:13px;margin-top:30px">' +
370
+ (_selectedId
371
+ ? 'No messages yet — type below to send'
372
+ : 'Select a peer to start messaging') +
373
+ '</div>';
374
+ return;
375
+ }
376
+ var html = '';
377
+ msgs.forEach(function (m) {
378
+ // .msg.q and .msg.a are defined in design-system.css
379
+ html += '<div class="msg ' + m.role + '">' + escapeHtml(m.text) + '</div>';
380
+ });
381
+ log.innerHTML = html;
382
+ log.scrollTop = log.scrollHeight;
383
+ }
384
+
385
+ // ── Send message to peer via POST /mesh/send ──────────────────────────────
386
+ function sendMessage() {
387
+ // CRIT C1: bail immediately if no peer selected
388
+ if (!_selectedId) return;
389
+ var inputEl = document.getElementById('od-mesh-cv-input');
390
+ if (!inputEl) return;
391
+ var text = inputEl.value.trim();
392
+ if (!text) return;
393
+
394
+ inputEl.value = '';
395
+
396
+ // Optimistic echo
397
+ if (!_convLogs[_selectedId]) _convLogs[_selectedId] = [];
398
+ _convLogs[_selectedId].push({ role: 'q', text: text });
399
+ renderConv();
400
+
401
+ var peerId = _selectedId; // capture before any async re-assign
402
+ meshFetch('/mesh/send', {
403
+ method: 'POST',
404
+ headers: { 'Content-Type': 'application/json' },
405
+ body: JSON.stringify({
406
+ from_peer: SEND_FROM,
407
+ to_peer: peerId,
408
+ content: text,
409
+ type: 'text'
410
+ })
411
+ })
412
+ .then(function (r) {
413
+ return r.json().then(function (d) { return { ok: r.ok, data: d }; });
414
+ })
415
+ .then(function (res) {
416
+ // CRIT C2: always surface errors — never silent
417
+ var ack = (res.ok && res.data && res.data.ok)
418
+ ? 'Delivered — queued in mesh (TTL 48h).'
419
+ : 'Error: ' + escapeHtml(
420
+ (res.data && res.data.detail) ? String(res.data.detail) : 'unknown error'
421
+ );
422
+ _convLogs[peerId].push({ role: 'a', text: ack });
423
+ renderConv();
424
+ })
425
+ .catch(function () {
426
+ // CRIT C2: network failure visible in log
427
+ _convLogs[peerId].push({ role: 'a', text: 'Network error — could not reach broker.' });
428
+ renderConv();
429
+ });
430
+ }
431
+
432
+ // ── Data loaders ──────────────────────────────────────────────────────────
433
+ function loadAll() {
434
+ var requestSeq = _requestSeq + 1;
435
+ _requestSeq = requestSeq;
436
+ meshFetch('/mesh/status')
437
+ .then(function (r) { return r.ok ? r.json() : null; })
438
+ .catch(function () { return null; })
439
+ .then(function (data) {
440
+ if (_requestSeq === requestSeq) renderPageDesc(data);
441
+ });
442
+
443
+ // The broker distinguishes remote LAN peers from local agent sessions.
444
+ // Both are real mesh participants and must be visible to operators.
445
+ meshFetch('/mesh/peers?view=all')
446
+ .then(function (r) { return r.ok ? r.json() : { peers: [] }; })
447
+ .catch(function () { return { peers: [] }; })
448
+ .then(function (d) {
449
+ if (_requestSeq !== requestSeq) return;
450
+ _peers = Array.isArray(d && d.peers) ? d.peers : [];
451
+ renderPeers(_peers);
452
+ // If the currently selected peer has expired, deselect
453
+ if (_selectedId && !_peers.find(function (p) { return p.peer_id === _selectedId; })) {
454
+ selectPeerById(null);
455
+ }
456
+ });
457
+ }
458
+
459
+ // ── Event wiring (CSP-safe delegation — no onXxx= attributes) ─────────────
460
+ function wireEvents(container) {
461
+ // Peer card click → select for conversation
462
+ var grid = container.querySelector('#od-mesh-peers-grid');
463
+ if (grid) {
464
+ grid.addEventListener('click', function (e) {
465
+ var card = e.target.closest('[data-peer-id]');
466
+ if (!card) return;
467
+ var pid = card.getAttribute('data-peer-id');
468
+ if (pid) selectPeerById(pid);
469
+ });
470
+ grid.addEventListener('keydown', function (e) {
471
+ if (e.key !== 'Enter' && e.key !== ' ') return;
472
+ var card = e.target.closest('[data-peer-id]');
473
+ if (!card) return;
474
+ e.preventDefault();
475
+ var pid = card.getAttribute('data-peer-id');
476
+ if (pid) selectPeerById(pid);
477
+ });
478
+ }
479
+
480
+ // Send button
481
+ var sendBtn = container.querySelector('#od-mesh-cv-send');
482
+ if (sendBtn) sendBtn.addEventListener('click', sendMessage);
483
+
484
+ // Enter key in input
485
+ var inputEl = container.querySelector('#od-mesh-cv-input');
486
+ if (inputEl) {
487
+ inputEl.addEventListener('keydown', function (e) {
488
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
489
+ });
490
+ }
491
+ }
492
+
493
+ // ── Refresh management: MutationObserver re-starts on tab activate ────────
494
+ function startRefreshObserver() {
495
+ var pane = document.getElementById('mesh-pane');
496
+ if (!pane || _observer) return;
497
+ _observer = new MutationObserver(function (mutations) {
498
+ mutations.forEach(function (m) {
499
+ if (m.type !== 'attributes' || m.attributeName !== 'class') return;
500
+ var t = m.target;
501
+ if (t.classList.contains('active') && t.classList.contains('show')) {
502
+ loadAll();
503
+ if (_timer) clearInterval(_timer);
504
+ _timer = setInterval(function () {
505
+ if (t.classList.contains('active')) {
506
+ loadAll();
507
+ } else {
508
+ clearInterval(_timer);
509
+ _timer = null;
510
+ }
511
+ }, REFRESH_MS);
512
+ }
513
+ });
514
+ });
515
+ _observer.observe(pane, { attributes: true });
516
+ }
517
+
518
+ // ── Public API ────────────────────────────────────────────────────────────
519
+ window.odRenderMesh = function (container) {
520
+ if (!container) return;
521
+ _container = container;
522
+
523
+ // Idempotent: if already rendered, only reload data
524
+ if (container.querySelector('#od-mesh-root')) {
525
+ loadAll();
526
+ return;
527
+ }
528
+
529
+ container.innerHTML = buildHTML();
530
+ wireEvents(container);
531
+ loadAll();
532
+ startRefreshObserver();
533
+
534
+ // Initial poll timer (self-stops when pane becomes inactive)
535
+ if (_timer) clearInterval(_timer);
536
+ _timer = setInterval(function () {
537
+ var pane = document.getElementById('mesh-pane');
538
+ if (pane && pane.classList.contains('active')) {
539
+ loadAll();
540
+ } else {
541
+ clearInterval(_timer);
542
+ _timer = null;
543
+ }
544
+ }, REFRESH_MS);
545
+ };
546
+
547
+ // Auto-run at DOMContentLoaded if the pane already exists in DOM
548
+ document.addEventListener('DOMContentLoaded', function () {
549
+ var pane = document.getElementById('mesh-pane');
550
+ if (pane) window.odRenderMesh(pane);
551
+ });
552
+
553
+ }());