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