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
@@ -25,6 +25,7 @@ import json
25
25
  import logging
26
26
  import os
27
27
  import socket
28
+ import sys
28
29
  import threading
29
30
  import time
30
31
  from pathlib import Path
@@ -35,6 +36,12 @@ logger = logging.getLogger(__name__)
35
36
 
36
37
  _DEFAULT_SOCK_NAME = "hook_daemon.sock"
37
38
 
39
+ # AF_UNIX is absent on Windows builds < 10.0.17063 and on Python < 3.9. When it
40
+ # is unavailable the hook daemon does not start and callers fall back to the
41
+ # subprocess recall path. Detect it explicitly (once, with a log) instead of
42
+ # relying on an AttributeError being swallowed by a broad except.
43
+ _AF_UNIX = getattr(socket, "AF_UNIX", None)
44
+
38
45
 
39
46
  def _default_sock_path() -> Path:
40
47
  return state_path(_DEFAULT_SOCK_NAME)
@@ -77,7 +84,13 @@ class HookDaemon:
77
84
  from superlocalmemory.core.recall_queue import RecallQueue
78
85
  self._queue = RecallQueue(self._queue_db_path)
79
86
 
80
- self._server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
87
+ if _AF_UNIX is None:
88
+ logger.info(
89
+ "HookDaemon: AF_UNIX unavailable on %s; hook recall uses the "
90
+ "subprocess fallback", sys.platform,
91
+ )
92
+ raise RuntimeError("AF_UNIX unavailable on this platform")
93
+ self._server_sock = socket.socket(_AF_UNIX, socket.SOCK_STREAM)
81
94
  self._server_sock.bind(str(self._sock_path))
82
95
  self._server_sock.listen(8)
83
96
  self._server_sock.settimeout(1.0)
@@ -226,9 +239,11 @@ def try_socket_recall(
226
239
  path = sock_path or _default_sock_path()
227
240
  if not path.exists():
228
241
  return None
242
+ if _AF_UNIX is None:
243
+ return None
229
244
 
230
245
  try:
231
- client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
246
+ client = socket.socket(_AF_UNIX, socket.SOCK_STREAM)
232
247
  client.settimeout(timeout)
233
248
  client.connect(str(path))
234
249
 
@@ -259,10 +274,12 @@ def ensure_hook_daemon(
259
274
  ) -> HookDaemon | None:
260
275
  """Start hook daemon if not already running. Returns daemon or None."""
261
276
  path = sock_path or _default_sock_path()
277
+ if _AF_UNIX is None:
278
+ return None
262
279
 
263
280
  if path.exists():
264
281
  try:
265
- test = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
282
+ test = socket.socket(_AF_UNIX, socket.SOCK_STREAM)
266
283
  test.settimeout(1.0)
267
284
  test.connect(str(path))
268
285
  test.close()
@@ -93,6 +93,58 @@ def memory_protocol_markdown() -> str:
93
93
  "learned>\", \"tags\": \"<comma-separated kebab-case keywords>\"}`.\n"
94
94
  "- A \"substantial task\" is anything you would write a commit "
95
95
  "message or handoff note about — not every tool call.\n"
96
+ "\n"
97
+ + optimize_protocol_markdown()
98
+ + "\n"
99
+ + loop_protocol_markdown()
100
+ )
101
+
102
+
103
+ def optimize_protocol_markdown() -> str:
104
+ """Return the agent-facing token-optimization protocol block (cache/compress).
105
+
106
+ Proxy-free: these MCP tools let the agent losslessly shrink large tool output
107
+ and reuse repeated reads through the same surface, so an agent on any
108
+ front-end (not just Claude Code) can save tokens without a proxy. Fail-open —
109
+ a non-ok result means "use the original and continue".
110
+ """
111
+ return (
112
+ "## Runtime token-optimization protocol (fail-open)\n"
113
+ "SLM can losslessly compress large tool output and cache repeated reads "
114
+ "through the same MCP surface — no proxy required. These calls only save "
115
+ "tokens; if one returns `ok: false`, use the original and continue.\n\n"
116
+ "- **Large tool output (>2000 chars)** → `hub__call_tool` with "
117
+ "`tool=\"slm__slm_compress\"` and `arguments={\"content\": \"<text>\", "
118
+ "\"mode\": \"auto\", \"reversible\": true}`; keep the returned `ccr_id` "
119
+ "and call `tool=\"slm__slm_retrieve\"` if you later need the full "
120
+ "original.\n"
121
+ "- **Repeated reads/searches** → `hub__call_tool` with "
122
+ "`tool=\"slm__slm_cache_get\"` and `arguments={\"key\": \"file:<path>\"}` "
123
+ "first; on a miss, store the result with `tool=\"slm__slm_cache_set\"` "
124
+ "(ttl ~1800).\n"
125
+ "- **Never compress or cache**: code you will edit, JSON you will parse, "
126
+ "secrets, ccr_ids, or anything under ~500 chars.\n"
127
+ )
128
+
129
+
130
+ def loop_protocol_markdown() -> str:
131
+ """Return the agent-facing bounded-loop protocol block.
132
+
133
+ Bounded loops make an agent stop when an INDEPENDENT gate passes — not when
134
+ the agent claims it is done. Persisting each lap to SLM memory makes a run
135
+ auditable and resumable. This block is appended to the shared memory
136
+ protocol so any connected front-end (Claude Code, Codex, Antigravity,
137
+ Cursor, and other IDEs) learns the feature exists and how to reach it.
138
+ """
139
+ return (
140
+ "## Runtime bounded-loop protocol\n"
141
+ "For a task with a checkable gate (tests, schema, lint, reconciliation), "
142
+ "run a *bounded loop*: iterate until an INDEPENDENT gate passes — never "
143
+ "on the agent's own claim, which is advisory only. Try `slm loop demo`; "
144
+ "inspect with `slm loop history` / `slm loop show <run_id>` (each lap "
145
+ "persists as SLM memory, tag `loop:<name>`). Statuses: DONE / HALT / "
146
+ "PAUSE / KILLED / ERROR — report exactly, never as success unless DONE. "
147
+ "Full guide: the slm-loop skill.\n"
96
148
  )
97
149
 
98
150
 
@@ -101,4 +153,6 @@ __all__ = (
101
153
  "SLM_MARKER_END",
102
154
  "strip_slm_block",
103
155
  "memory_protocol_markdown",
156
+ "optimize_protocol_markdown",
157
+ "loop_protocol_markdown",
104
158
  )
@@ -139,7 +139,7 @@ IDE_MATRIX: dict[str, IDEDescriptor] = {
139
139
  mcp_path_project=".mcp.json",
140
140
  server_key="mcpServers",
141
141
  fmt="json",
142
- agents_md_path=None,
142
+ agents_md_path=".junie/AGENTS.md", # Junie guidelines file (GA) — carries memory + optimize protocol
143
143
  server_block={"command": "slm", "args": ["mcp"], "type": "stdio"},
144
144
  caveats="path per product [CN-ONLINE]",
145
145
  ),
@@ -347,6 +347,54 @@ def connect_ide(
347
347
  return result
348
348
 
349
349
 
350
+ def connect_many(
351
+ ide_ids: list[str],
352
+ *,
353
+ home: Path | None = None,
354
+ project: Path | None = None,
355
+ here: bool = False,
356
+ profile: str | None = None,
357
+ agents_md_source: Callable[[], str] | None = None,
358
+ ) -> list[dict[str, Any]]:
359
+ """Wire SLM into multiple IDE configs via non-destructive merge.
360
+
361
+ Iterates over ``ide_ids`` and calls :func:`connect_ide` for each entry.
362
+ Each IDE is processed independently — a failure on one IDE does NOT abort
363
+ the remaining targets.
364
+
365
+ The underlying :func:`connect_ide` is MERGE-NOT-CLOBBER:
366
+ - Only the ``superlocalmemory`` server key is touched.
367
+ - All other MCP servers + top-level keys are preserved byte-for-byte.
368
+ - Writes are atomic (.tmp + os.replace).
369
+
370
+ Args:
371
+ ide_ids: IDE ids to wire (from :data:`IDE_MATRIX`). Pass an empty
372
+ list to no-op. Unknown ids produce error entries in the output.
373
+ home: Override ``$HOME`` (test hook).
374
+ project: Project root for ``here=True`` installs.
375
+ here: When True, write to project-relative path instead of global.
376
+ profile: Inject ``SLM_MCP_PROFILE`` env-var into every server block.
377
+ agents_md_source: Callable returning AGENTS.md content to append.
378
+
379
+ Returns:
380
+ List of per-IDE result dicts, one per input id. Each dict has the
381
+ same shape as :func:`connect_ide`'s return value::
382
+
383
+ {ide, mcp_config, mcp_path, agents_md, servers_preserved, error}
384
+ """
385
+ return [
386
+ connect_ide(
387
+ ide_id,
388
+ home=home,
389
+ project=project,
390
+ here=here,
391
+ profile=profile,
392
+ agents_md_source=agents_md_source,
393
+ )
394
+ for ide_id in ide_ids
395
+ ]
396
+
397
+
350
398
  # ---------------------------------------------------------------------------
351
399
  # Internal helpers
352
400
  # ---------------------------------------------------------------------------
@@ -504,3 +552,68 @@ def _handle_agents_md(
504
552
  _tmp.write_text(existing + section, encoding="utf-8")
505
553
  os.replace(_tmp, agents_path)
506
554
  return "wrote"
555
+
556
+
557
+ # ---------------------------------------------------------------------------
558
+ # __main__ — `python -m superlocalmemory.hooks.portable_kit [ids...]`
559
+ #
560
+ # Used by the npm installer to execute non-destructive IDE connects when
561
+ # Python is available at postinstall time. If Python / the package is not
562
+ # importable the installer records a pending_ide_connections.json instead.
563
+ # ---------------------------------------------------------------------------
564
+
565
+ if __name__ == "__main__": # pragma: no cover
566
+ import argparse as _argparse
567
+
568
+ _parser = _argparse.ArgumentParser(
569
+ description="SLM IDE connector — non-destructive merge-not-clobber."
570
+ )
571
+ _parser.add_argument(
572
+ "ide_ids",
573
+ nargs="*",
574
+ metavar="IDE",
575
+ help=(
576
+ "IDE ids to connect. Pass 'all' to connect every supported IDE "
577
+ "(excluding claude-code which uses the WP-06 plugin)."
578
+ ),
579
+ )
580
+ _parser.add_argument("--home", help="Override home directory (test hook).")
581
+ _parser.add_argument("--profile", help="SLM_MCP_PROFILE to inject.")
582
+ _parser.add_argument(
583
+ "--list", action="store_true",
584
+ help="Print supported IDE ids with display names and exit.",
585
+ )
586
+ _args = _parser.parse_args()
587
+
588
+ if _args.list:
589
+ for _id, _desc in IDE_MATRIX.items():
590
+ _flag = "[OUT]" if not _desc.fmt else ""
591
+ print(f" {_id:22s} {_desc.display} {_flag}".rstrip())
592
+ sys.exit(0)
593
+
594
+ _home = Path(_args.home) if _args.home else None
595
+
596
+ # Resolve targets: "all" → every MCP-capable IDE (fmt != ""), else explicit list.
597
+ if "all" in _args.ide_ids:
598
+ _targets = [k for k, d in IDE_MATRIX.items() if d.fmt]
599
+ else:
600
+ _targets = [t for t in _args.ide_ids if t]
601
+
602
+ if not _targets:
603
+ print("No IDE ids given. Pass ide ids or 'all'. Use --list to see options.")
604
+ sys.exit(0)
605
+
606
+ _results = connect_many(_targets, home=_home, profile=_args.profile)
607
+ _ok = 0
608
+ _fail = 0
609
+ for _r in _results:
610
+ _err = _r.get("error")
611
+ _status = _r.get("mcp_config", "error")
612
+ if _err:
613
+ print(f" ERROR {_r['ide']}: {_err}", file=sys.stderr)
614
+ _fail += 1
615
+ else:
616
+ print(f" {_status.upper():9s} {_r['ide']} → {_r.get('mcp_path', '')}")
617
+ _ok += 1
618
+ print(f"\n{_ok} connected, {_fail} errors.")
619
+ sys.exit(0 if _fail == 0 else 1)
@@ -15,6 +15,7 @@ V3 change: base directory moved from ``~/.claude-memory/`` to
15
15
  import hashlib
16
16
  import hmac
17
17
  import logging
18
+ import os
18
19
  from pathlib import Path
19
20
  from typing import Optional
20
21
 
@@ -26,6 +27,13 @@ logger = logging.getLogger("superlocalmemory.auth")
26
27
  MEMORY_DIR = DynamicStatePath()
27
28
  API_KEY_FILE = DynamicStatePath("api_key")
28
29
 
30
+ # v3.7.8 (F1): opt-in env flag that restores the pre-v3.7.6 shared-host
31
+ # posture -- when set AND an api_key file is configured, uncredentialed
32
+ # loopback writes must also present a matching X-SLM-API-Key. Default OFF
33
+ # preserves the v3.7.6 local-first fix (#71/#73/#74): loopback callers are
34
+ # trusted as the local OS-user boundary without needing any credential.
35
+ SLM_REQUIRE_API_KEY_LOOPBACK_ENV = "SLM_REQUIRE_API_KEY_LOOPBACK"
36
+
29
37
 
30
38
  def _load_api_key_hash(key_file: Optional[Path] = None) -> Optional[str]:
31
39
  """Load and hash the API key from disk.
@@ -96,6 +104,26 @@ def verify_api_key(
96
104
  return hmac.compare_digest(actual, expected)
97
105
 
98
106
 
107
+ def loopback_strict_mode_enabled(key_file: Optional[Path] = None) -> bool:
108
+ """Whether uncredentialed loopback writes must present the API key.
109
+
110
+ v3.7.8 (F1/F2): opt-in via ``SLM_REQUIRE_API_KEY_LOOPBACK`` (any of
111
+ "1"/"true"/"yes"/"on", case-insensitive). This is the sole enforcement
112
+ point for the strict shared-host posture -- it is checked ONLY for the
113
+ uncredentialed-loopback case (a caller presenting none of
114
+ X-SLM-Daemon-Capability / X-Install-Token / X-SLM-API-Key). Callers who
115
+ already present a valid capability or install token are unaffected: those
116
+ are stronger, explicit credentials and this flag never re-litigates them.
117
+
118
+ Returns ``False`` (no-op) when the flag is unset/false, OR when no
119
+ api_key file is configured -- there is nothing to require in that case.
120
+ """
121
+ raw = os.environ.get(SLM_REQUIRE_API_KEY_LOOPBACK_ENV, "")
122
+ if raw.strip().lower() not in ("1", "true", "yes", "on"):
123
+ return False
124
+ return _load_api_key_hash(key_file) is not None
125
+
126
+
99
127
  def authorize_http_mcp_request(
100
128
  request_headers: dict,
101
129
  *,
@@ -266,7 +266,18 @@ class BackupManager:
266
266
 
267
267
  A safety snapshot of the current state is taken first.
268
268
  """
269
- backup_path = self.backup_dir / filename
269
+ # Containment: filename must be a bare .db name inside backup_dir no
270
+ # path separators or traversal. Prevents restoring (and thus copying
271
+ # over memory.db) an arbitrary file the daemon user can read.
272
+ if (not filename or "/" in filename or "\\" in filename
273
+ or ".." in filename or not filename.endswith(".db")):
274
+ logger.error("Restore rejected: invalid backup filename: %r", filename)
275
+ return False
276
+ backup_dir = self.backup_dir.resolve()
277
+ backup_path = (self.backup_dir / filename).resolve()
278
+ if backup_path.parent != backup_dir:
279
+ logger.error("Restore rejected: path escapes backup dir: %r", filename)
280
+ return False
270
281
  if not backup_path.exists():
271
282
  logger.error("Backup not found: %s", filename)
272
283
  return False
@@ -14,8 +14,11 @@ import getpass
14
14
  import hashlib
15
15
  import hmac
16
16
  import json
17
+ import logging
17
18
  import os
18
19
  import secrets
20
+ import subprocess
21
+ import sys
19
22
  import time
20
23
  import uuid
21
24
  from dataclasses import asdict, dataclass
@@ -30,6 +33,8 @@ DAEMON_SERVICE = "superlocalmemory-daemon"
30
33
  _NAMESPACE_DOMAIN = b"superlocalmemory-daemon-namespace-v1\0"
31
34
  _CAPABILITY_DOMAIN = b"superlocalmemory-daemon-capability-v1\0"
32
35
 
36
+ logger = logging.getLogger(__name__)
37
+
33
38
 
34
39
  def _canonical_path(value: str | Path) -> Path:
35
40
  expanded = Path(value).expanduser().resolve(strict=False)
@@ -144,6 +149,40 @@ def descriptor_path(data_root: str | Path | None = None) -> Path:
144
149
  return root / "daemon.json"
145
150
 
146
151
 
152
+ def _restrict_to_owner(path: Path) -> None:
153
+ """Restrict a sensitive file to the current user on every platform.
154
+
155
+ ``daemon.json`` holds the capability token that authorizes write requests.
156
+ POSIX gets a 0600 chmod. On Windows chmod is a no-op, so use icacls to strip
157
+ inherited ACEs and grant the current user only — otherwise the token can be
158
+ read by other local accounts (privilege escalation on shared machines).
159
+ Files under %USERPROFILE% usually inherit user-only ACLs already; this is
160
+ defense-in-depth. Fail-soft — a hardening failure warns, never crashes.
161
+ """
162
+ if sys.platform == "win32":
163
+ try:
164
+ user = getpass.getuser()
165
+ subprocess.run(
166
+ ["icacls", str(path), "/inheritance:r"],
167
+ check=False, capture_output=True,
168
+ )
169
+ if user:
170
+ subprocess.run(
171
+ ["icacls", str(path), "/grant:r", f"{user}:F"],
172
+ check=False, capture_output=True,
173
+ )
174
+ except Exception as exc: # noqa: BLE001
175
+ logger.warning(
176
+ "could not restrict ACL on %s (%s); the capability token may be "
177
+ "readable by other local users", path, exc,
178
+ )
179
+ else:
180
+ try:
181
+ os.chmod(path, 0o600)
182
+ except OSError as exc:
183
+ logger.warning("could not chmod %s to 0600: %s", path, exc)
184
+
185
+
147
186
  def write_descriptor(
148
187
  descriptor: DaemonDescriptor,
149
188
  *,
@@ -162,10 +201,7 @@ def write_descriptor(
162
201
  stream.flush()
163
202
  os.fsync(stream.fileno())
164
203
  os.replace(temporary, destination)
165
- try:
166
- os.chmod(destination, 0o600)
167
- except OSError:
168
- pass
204
+ _restrict_to_owner(destination)
169
205
  finally:
170
206
  temporary.unlink(missing_ok=True)
171
207
  return destination
@@ -11,6 +11,7 @@ explicit process contract and always wins over persisted configuration.
11
11
  from __future__ import annotations
12
12
 
13
13
  import json
14
+ import logging
14
15
  import os
15
16
  from pathlib import Path
16
17
 
@@ -24,6 +25,8 @@ _DURABLE_IDENTITY_NAMES = frozenset(
24
25
  },
25
26
  )
26
27
 
28
+ logger = logging.getLogger(__name__)
29
+
27
30
 
28
31
  class DataRootConflictError(RuntimeError):
29
32
  """Raised when two state-bearing roots make startup ambiguous."""
@@ -122,17 +125,53 @@ def assert_no_durable_root_conflict(
122
125
  *,
123
126
  home: str | Path | None = None,
124
127
  ) -> None:
125
- """Refuse ambiguous startup when selected and default roots hold state.
126
-
127
- This check never writes, copies, or deletes data. A default root that only
128
- contains the legacy relocation config is safe and does not trigger it.
128
+ """Refuse *ambiguous* startup when two state roots make the live namespace unclear.
129
+
130
+ The root actually selected for this process is always inspected: an
131
+ unreadable selected root fails closed. Beyond that, a conflict is only raised
132
+ when the selection was *implicit* — resolved from the legacy
133
+ ``config.json:base_dir`` relocation hint — and a separately-populated default
134
+ root leaves it genuinely ambiguous which namespace is live.
135
+
136
+ An explicit environment selection (``SLM_DATA_DIR`` / ``SL_MEMORY_PATH`` /
137
+ ``SLM_HOME``) is an unambiguous operator contract: a separately-populated
138
+ default root is then a deliberate multi-root / per-team / second-instance
139
+ layout, not an ambiguity, so startup proceeds. If the explicitly chosen root
140
+ is empty while the old default still holds data, that likely-mistyped path is
141
+ surfaced as a warning rather than a hard block.
142
+
143
+ This check never writes, copies, or deletes data.
129
144
  """
130
145
  home_path = _canonical_path(home if home is not None else Path.home())
131
146
  default_root = _canonical_path(home_path / ".superlocalmemory")
132
147
  selected_root = canonical_data_root(home=home_path)
133
148
  if selected_root == default_root:
134
149
  return
150
+
151
+ # The root about to be used must be inspectable regardless of how it was
152
+ # chosen; an unreadable selected root fails closed inside _durable_markers.
135
153
  selected_markers = _durable_markers(selected_root)
154
+
155
+ if environment_data_root() is not None:
156
+ # Explicit selection wins; a populated default root is a deliberate
157
+ # multi-root layout, not an ambiguity. Only warn on the "empty new root
158
+ # while the old default still holds data" case so a wrong SLM_DATA_DIR
159
+ # stays visible. Inspection of the unused default never blocks startup.
160
+ if not selected_markers:
161
+ try:
162
+ default_has_data = bool(_durable_markers(default_root))
163
+ except DataRootConflictError:
164
+ default_has_data = False
165
+ if default_has_data:
166
+ logger.warning(
167
+ "SLM_DATA_DIR selects an empty state root (%s) while the "
168
+ "default root (%s) still holds data; starting with the empty "
169
+ "root as explicitly requested.",
170
+ selected_root,
171
+ default_root,
172
+ )
173
+ return
174
+
136
175
  default_markers = _durable_markers(default_root)
137
176
  if not selected_markers or not default_markers:
138
177
  return