superlocalmemory 3.4.19 → 3.4.22

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 (177) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +42 -34
  3. package/bin/slm +11 -0
  4. package/bin/slm.bat +12 -0
  5. package/package.json +4 -3
  6. package/pyproject.toml +4 -3
  7. package/scripts/build-slm-hook.ps1 +40 -0
  8. package/scripts/build-slm-hook.sh +45 -0
  9. package/scripts/build_entry.py +452 -0
  10. package/scripts/ci/stage5b_gate.sh +50 -0
  11. package/scripts/postinstall/validation.js +187 -0
  12. package/scripts/postinstall-interactive.js +756 -0
  13. package/scripts/postinstall_binary.js +287 -0
  14. package/scripts/release_manifest.py +273 -0
  15. package/scripts/slm-hook.spec +56 -0
  16. package/skills/slm-build-graph/SKILL.md +423 -0
  17. package/skills/slm-list-recent/SKILL.md +348 -0
  18. package/skills/slm-recall/SKILL.md +343 -0
  19. package/skills/slm-remember/SKILL.md +194 -0
  20. package/skills/slm-show-patterns/SKILL.md +224 -0
  21. package/skills/slm-status/SKILL.md +363 -0
  22. package/skills/slm-switch-profile/SKILL.md +442 -0
  23. package/src/superlocalmemory/cli/commands.py +254 -79
  24. package/src/superlocalmemory/cli/context_commands.py +192 -0
  25. package/src/superlocalmemory/cli/daemon.py +15 -1
  26. package/src/superlocalmemory/cli/db_migrate.py +80 -0
  27. package/src/superlocalmemory/cli/escape_hatch.py +220 -0
  28. package/src/superlocalmemory/cli/main.py +72 -1
  29. package/src/superlocalmemory/core/context_cache.py +397 -0
  30. package/src/superlocalmemory/core/engine.py +38 -2
  31. package/src/superlocalmemory/core/engine_wiring.py +1 -1
  32. package/src/superlocalmemory/core/ram_lock.py +111 -0
  33. package/src/superlocalmemory/core/recall_pipeline.py +433 -3
  34. package/src/superlocalmemory/core/recall_worker.py +8 -3
  35. package/src/superlocalmemory/core/security_primitives.py +635 -0
  36. package/src/superlocalmemory/core/shadow_router.py +319 -0
  37. package/src/superlocalmemory/core/slm_disabled.py +87 -0
  38. package/src/superlocalmemory/core/slmignore.py +125 -0
  39. package/src/superlocalmemory/core/topic_signature.py +143 -0
  40. package/src/superlocalmemory/core/worker_pool.py +14 -3
  41. package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
  42. package/src/superlocalmemory/evolution/budget.py +321 -0
  43. package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
  44. package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
  45. package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
  46. package/src/superlocalmemory/hooks/adapter_base.py +317 -0
  47. package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
  48. package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
  49. package/src/superlocalmemory/hooks/context_payload.py +312 -0
  50. package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
  51. package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
  52. package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
  53. package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
  54. package/src/superlocalmemory/hooks/ide_connector.py +25 -2
  55. package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
  56. package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
  57. package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
  58. package/src/superlocalmemory/hooks/session_registry.py +186 -0
  59. package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
  60. package/src/superlocalmemory/hooks/sync_loop.py +114 -0
  61. package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
  62. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
  63. package/src/superlocalmemory/infra/backup.py +3 -3
  64. package/src/superlocalmemory/infra/cloud_backup.py +2 -2
  65. package/src/superlocalmemory/infra/event_bus.py +2 -2
  66. package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
  67. package/src/superlocalmemory/learning/arm_catalog.py +99 -0
  68. package/src/superlocalmemory/learning/bandit.py +526 -0
  69. package/src/superlocalmemory/learning/bandit_cache.py +133 -0
  70. package/src/superlocalmemory/learning/behavioral.py +53 -1
  71. package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
  72. package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
  73. package/src/superlocalmemory/learning/database.py +256 -0
  74. package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
  75. package/src/superlocalmemory/learning/ensemble.py +300 -0
  76. package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
  77. package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
  78. package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
  79. package/src/superlocalmemory/learning/labeler.py +87 -0
  80. package/src/superlocalmemory/learning/legacy_migration.py +277 -0
  81. package/src/superlocalmemory/learning/memory_merge.py +160 -0
  82. package/src/superlocalmemory/learning/model_cache.py +269 -0
  83. package/src/superlocalmemory/learning/model_rollback.py +278 -0
  84. package/src/superlocalmemory/learning/outcome_queue.py +284 -0
  85. package/src/superlocalmemory/learning/pattern_miner.py +415 -0
  86. package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
  87. package/src/superlocalmemory/learning/ranker.py +225 -81
  88. package/src/superlocalmemory/learning/ranker_common.py +163 -0
  89. package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
  90. package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
  91. package/src/superlocalmemory/learning/reward.py +777 -0
  92. package/src/superlocalmemory/learning/reward_archive.py +210 -0
  93. package/src/superlocalmemory/learning/reward_boost.py +201 -0
  94. package/src/superlocalmemory/learning/reward_proxy.py +326 -0
  95. package/src/superlocalmemory/learning/shadow_test.py +524 -0
  96. package/src/superlocalmemory/learning/signal_worker.py +270 -0
  97. package/src/superlocalmemory/learning/signals.py +314 -0
  98. package/src/superlocalmemory/learning/trigram_index.py +547 -0
  99. package/src/superlocalmemory/mcp/server.py +5 -5
  100. package/src/superlocalmemory/mcp/tools_context.py +183 -0
  101. package/src/superlocalmemory/mcp/tools_core.py +92 -27
  102. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
  103. package/src/superlocalmemory/retrieval/engine.py +52 -0
  104. package/src/superlocalmemory/server/api.py +2 -2
  105. package/src/superlocalmemory/server/bandit_loops.py +140 -0
  106. package/src/superlocalmemory/server/middleware/__init__.py +11 -0
  107. package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
  108. package/src/superlocalmemory/server/routes/backup.py +36 -13
  109. package/src/superlocalmemory/server/routes/behavioral.py +50 -19
  110. package/src/superlocalmemory/server/routes/brain.py +1234 -0
  111. package/src/superlocalmemory/server/routes/data_io.py +4 -4
  112. package/src/superlocalmemory/server/routes/events.py +2 -2
  113. package/src/superlocalmemory/server/routes/helpers.py +1 -1
  114. package/src/superlocalmemory/server/routes/learning.py +192 -7
  115. package/src/superlocalmemory/server/routes/memories.py +189 -1
  116. package/src/superlocalmemory/server/routes/prewarm.py +171 -0
  117. package/src/superlocalmemory/server/routes/profiles.py +3 -3
  118. package/src/superlocalmemory/server/routes/token.py +88 -0
  119. package/src/superlocalmemory/server/routes/ws.py +5 -5
  120. package/src/superlocalmemory/server/security_middleware.py +13 -7
  121. package/src/superlocalmemory/server/ui.py +2 -2
  122. package/src/superlocalmemory/server/unified_daemon.py +335 -3
  123. package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
  124. package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
  125. package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
  126. package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
  127. package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
  128. package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
  129. package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
  130. package/src/superlocalmemory/storage/migration_runner.py +545 -0
  131. package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
  132. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
  133. package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
  134. package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
  135. package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
  136. package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
  137. package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
  138. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
  139. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
  140. package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
  141. package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
  142. package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
  143. package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
  144. package/src/superlocalmemory/storage/models.py +4 -0
  145. package/src/superlocalmemory/ui/css/brain.css +409 -0
  146. package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
  147. package/src/superlocalmemory/ui/index.html +459 -1345
  148. package/src/superlocalmemory/ui/js/brain.js +1321 -0
  149. package/src/superlocalmemory/ui/js/clusters.js +123 -4
  150. package/src/superlocalmemory/ui/js/init.js +48 -39
  151. package/src/superlocalmemory/ui/js/memories.js +88 -2
  152. package/src/superlocalmemory/ui/js/modal.js +71 -1
  153. package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
  154. package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
  155. package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
  156. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  157. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  158. package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
  159. package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
  160. package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
  161. package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
  162. package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
  163. package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
  164. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
  165. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
  166. package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
  167. package/src/superlocalmemory/ui/js/behavioral.js +0 -447
  168. package/src/superlocalmemory/ui/js/graph-core.js +0 -447
  169. package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
  170. package/src/superlocalmemory/ui/js/learning.js +0 -435
  171. package/src/superlocalmemory/ui/js/patterns.js +0 -93
  172. package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
  173. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
  174. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  175. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  176. package/src/superlocalmemory.egg-info/requires.txt +0 -58
  177. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -0,0 +1,312 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.22 — LLD-05 §3
4
+
5
+ """Shared content builder — single source for every adapter body.
6
+
7
+ LLD reference: ``.backup/active-brain/lld/LLD-05-cross-platform-adapters.md``
8
+ Section 3 (Content Builder). One builder → five formatters. Every string is
9
+ passed through ``redact_secrets`` before entering the dataclass, so no
10
+ adapter ever writes an unredacted secret.
11
+
12
+ Hard rule A9: secret redaction applied to payload before write.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+ from typing import Callable, Iterable
21
+
22
+ from superlocalmemory.core.security_primitives import redact_secrets
23
+
24
+
25
+ VERSION = "3.4.22"
26
+ DEFAULT_TOP_K = 10
27
+ DEFAULT_DECISIONS_K = 5
28
+ DEFAULT_MEMORIES_K = 10
29
+
30
+ # A RecallFn takes (query, limit, profile_id) and returns a list of memory
31
+ # dicts with at least {"text": str, "score": float}. Adapters inject the
32
+ # real recall engine at construction time; tests inject a fake.
33
+ RecallFn = Callable[[str, int, str], list[dict]]
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class ContextPayload:
38
+ """Normalised, redacted context ready for any adapter to format.
39
+
40
+ All strings are post-redaction. Topics and entities are ranked tuples to
41
+ keep the structure immutable and deterministically serialisable.
42
+ """
43
+
44
+ profile_id: str
45
+ topics: tuple[tuple[str, float], ...]
46
+ entities: tuple[tuple[str, int], ...]
47
+ recent_decisions: tuple[str, ...]
48
+ project_memories: tuple[str, ...]
49
+ generated_at: str
50
+ version: str = VERSION
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Builder
55
+ # ---------------------------------------------------------------------------
56
+
57
+
58
+ def _now_iso() -> str:
59
+ return datetime.now(timezone.utc).isoformat()
60
+
61
+
62
+ def _redact_str(s: str) -> str:
63
+ return redact_secrets(s) if isinstance(s, str) else ""
64
+
65
+
66
+ def _redact_seq(items: Iterable[str], limit: int) -> tuple[str, ...]:
67
+ cleaned: list[str] = []
68
+ for item in items:
69
+ if not isinstance(item, str) or not item:
70
+ continue
71
+ cleaned.append(_redact_str(item))
72
+ if len(cleaned) >= limit:
73
+ break
74
+ return tuple(cleaned)
75
+
76
+
77
+ def _recall_topics(
78
+ recall_fn: RecallFn, profile_id: str, scope: str, limit: int,
79
+ ) -> tuple[tuple[str, float], ...]:
80
+ query = "topics" if scope == "global" else "project topics"
81
+ try:
82
+ results = recall_fn(query, limit, profile_id) or []
83
+ except Exception:
84
+ return ()
85
+ topics: list[tuple[str, float]] = []
86
+ for row in results:
87
+ if not isinstance(row, dict):
88
+ continue
89
+ name = row.get("name") or row.get("text") or ""
90
+ if not isinstance(name, str) or not name:
91
+ continue
92
+ strength = float(row.get("score", row.get("strength", 0.0)) or 0.0)
93
+ topics.append((_redact_str(name), strength))
94
+ if len(topics) >= limit:
95
+ break
96
+ return tuple(topics)
97
+
98
+
99
+ def _recall_entities(
100
+ recall_fn: RecallFn, profile_id: str, scope: str, limit: int,
101
+ ) -> tuple[tuple[str, int], ...]:
102
+ query = "entities" if scope == "global" else "project entities"
103
+ try:
104
+ results = recall_fn(query, limit, profile_id) or []
105
+ except Exception:
106
+ return ()
107
+ entities: list[tuple[str, int]] = []
108
+ for row in results:
109
+ if not isinstance(row, dict):
110
+ continue
111
+ name = row.get("name") or row.get("text") or ""
112
+ if not isinstance(name, str) or not name:
113
+ continue
114
+ mentions = int(row.get("mentions", row.get("count", 0)) or 0)
115
+ entities.append((_redact_str(name), mentions))
116
+ if len(entities) >= limit:
117
+ break
118
+ return tuple(entities)
119
+
120
+
121
+ def _recall_decisions(
122
+ recall_fn: RecallFn, profile_id: str, limit: int,
123
+ ) -> tuple[str, ...]:
124
+ try:
125
+ rows = recall_fn("recent decisions", limit, profile_id) or []
126
+ except Exception:
127
+ return ()
128
+ texts = (row.get("text", "") for row in rows if isinstance(row, dict))
129
+ return _redact_seq(texts, limit)
130
+
131
+
132
+ def _recall_memories(
133
+ recall_fn: RecallFn, profile_id: str, scope: str, limit: int,
134
+ ) -> tuple[str, ...]:
135
+ query = "project memories" if scope == "project" else "memories"
136
+ try:
137
+ rows = recall_fn(query, limit, profile_id) or []
138
+ except Exception:
139
+ return ()
140
+ texts = (row.get("text", "") for row in rows if isinstance(row, dict))
141
+ return _redact_seq(texts, limit)
142
+
143
+
144
+ def build_payload(
145
+ profile_id: str,
146
+ scope: str,
147
+ cwd: Path,
148
+ *,
149
+ recall_fn: RecallFn,
150
+ top_k: int = DEFAULT_TOP_K,
151
+ decisions_k: int = DEFAULT_DECISIONS_K,
152
+ memories_k: int = DEFAULT_MEMORIES_K,
153
+ now_fn: Callable[[], str] | None = None,
154
+ ) -> ContextPayload:
155
+ """Build a redacted, ranked context payload.
156
+
157
+ ``scope`` ∈ {"project", "global"}. ``cwd`` is informational for the
158
+ recall engine (engine-specific signals can key off it); the builder
159
+ itself is a pure transform. ``recall_fn`` is injected — adapters wire
160
+ it to the real engine, tests wire a fake.
161
+ """
162
+ if scope not in ("project", "global"):
163
+ raise ValueError(f"scope must be 'project' or 'global', got {scope!r}")
164
+
165
+ topics = _recall_topics(recall_fn, profile_id, scope, top_k)
166
+ entities = _recall_entities(recall_fn, profile_id, scope, top_k)
167
+ decisions = _recall_decisions(recall_fn, profile_id, decisions_k)
168
+ memories = _recall_memories(recall_fn, profile_id, scope, memories_k)
169
+
170
+ # Late-bind ``now_fn`` so monkeypatching ``_now_iso`` at module scope
171
+ # still controls the timestamp — crucial for deterministic content-hash
172
+ # tests across sync attempts.
173
+ ts_fn = now_fn if now_fn is not None else _now_iso
174
+
175
+ return ContextPayload(
176
+ profile_id=profile_id,
177
+ topics=topics,
178
+ entities=entities,
179
+ recent_decisions=decisions,
180
+ project_memories=memories,
181
+ generated_at=ts_fn(),
182
+ version=VERSION,
183
+ )
184
+
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Formatting helpers
188
+ # ---------------------------------------------------------------------------
189
+
190
+
191
+ def format_topics(payload: ContextPayload, limit: int = DEFAULT_TOP_K) -> str:
192
+ if not payload.topics:
193
+ return "_(none yet)_"
194
+ lines = []
195
+ for name, strength in payload.topics[:limit]:
196
+ lines.append(f"- {name} ({strength:.2f})")
197
+ return "\n".join(lines)
198
+
199
+
200
+ def format_entities(
201
+ payload: ContextPayload, limit: int = DEFAULT_TOP_K,
202
+ ) -> str:
203
+ if not payload.entities:
204
+ return "_(none yet)_"
205
+ lines = []
206
+ for name, mentions in payload.entities[:limit]:
207
+ lines.append(f"- {name} ({mentions})")
208
+ return "\n".join(lines)
209
+
210
+
211
+ def format_decisions(
212
+ payload: ContextPayload, limit: int = DEFAULT_DECISIONS_K,
213
+ ) -> str:
214
+ if not payload.recent_decisions:
215
+ return "_(none yet)_"
216
+ return "\n".join(f"- {d}" for d in payload.recent_decisions[:limit])
217
+
218
+
219
+ def format_memories(
220
+ payload: ContextPayload, limit: int = DEFAULT_MEMORIES_K,
221
+ ) -> str:
222
+ if not payload.project_memories:
223
+ return "_(none yet)_"
224
+ return "\n".join(f"- {m}" for m in payload.project_memories[:limit])
225
+
226
+
227
+ def truncate_payload_for_cap(
228
+ payload: ContextPayload, *, hard_cap: int, render: Callable[[ContextPayload], bytes],
229
+ ) -> bytes:
230
+ """Repeatedly drop sections until ``render(payload)`` fits ``hard_cap``.
231
+
232
+ Truncation order (LLD-05 §4.3): project_memories → recent_decisions →
233
+ entities → topics (topics are kept if at all possible).
234
+ """
235
+ rendered = render(payload)
236
+ if len(rendered) <= hard_cap:
237
+ return rendered
238
+
239
+ # 1. trim project_memories
240
+ p = _with_memories(payload, ())
241
+ rendered = render(p)
242
+ if len(rendered) <= hard_cap:
243
+ return rendered
244
+
245
+ # 2. trim recent_decisions
246
+ p = _with_decisions(p, ())
247
+ rendered = render(p)
248
+ if len(rendered) <= hard_cap:
249
+ return rendered
250
+
251
+ # 3. trim entities
252
+ p = _with_entities(p, ())
253
+ rendered = render(p)
254
+ if len(rendered) <= hard_cap:
255
+ return rendered
256
+
257
+ # 4. (as last resort) trim topics too
258
+ p = _with_topics(p, ())
259
+ rendered = render(p)
260
+ return rendered # caller applies truncate_to_cap safety net
261
+
262
+
263
+ def _with_memories(p: ContextPayload,
264
+ memories: tuple[str, ...]) -> ContextPayload:
265
+ return ContextPayload(
266
+ profile_id=p.profile_id, topics=p.topics, entities=p.entities,
267
+ recent_decisions=p.recent_decisions, project_memories=memories,
268
+ generated_at=p.generated_at, version=p.version,
269
+ )
270
+
271
+
272
+ def _with_decisions(p: ContextPayload,
273
+ decisions: tuple[str, ...]) -> ContextPayload:
274
+ return ContextPayload(
275
+ profile_id=p.profile_id, topics=p.topics, entities=p.entities,
276
+ recent_decisions=decisions, project_memories=p.project_memories,
277
+ generated_at=p.generated_at, version=p.version,
278
+ )
279
+
280
+
281
+ def _with_entities(p: ContextPayload,
282
+ entities: tuple[tuple[str, int], ...]) -> ContextPayload:
283
+ return ContextPayload(
284
+ profile_id=p.profile_id, topics=p.topics, entities=entities,
285
+ recent_decisions=p.recent_decisions, project_memories=p.project_memories,
286
+ generated_at=p.generated_at, version=p.version,
287
+ )
288
+
289
+
290
+ def _with_topics(p: ContextPayload,
291
+ topics: tuple[tuple[str, float], ...]) -> ContextPayload:
292
+ return ContextPayload(
293
+ profile_id=p.profile_id, topics=topics, entities=p.entities,
294
+ recent_decisions=p.recent_decisions, project_memories=p.project_memories,
295
+ generated_at=p.generated_at, version=p.version,
296
+ )
297
+
298
+
299
+ __all__ = (
300
+ "ContextPayload",
301
+ "DEFAULT_DECISIONS_K",
302
+ "DEFAULT_MEMORIES_K",
303
+ "DEFAULT_TOP_K",
304
+ "RecallFn",
305
+ "VERSION",
306
+ "build_payload",
307
+ "format_decisions",
308
+ "format_entities",
309
+ "format_memories",
310
+ "format_topics",
311
+ "truncate_payload_for_cap",
312
+ )
@@ -0,0 +1,154 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.22 — LLD-05 §6
4
+
5
+ """Copilot adapter — writes ``.github/copilot-instructions.md``.
6
+
7
+ LLD-05 §6. Verified (verification-2026-04-17.md claim 5): plain markdown,
8
+ no frontmatter, soft 2 KB / hard 4 KB cap. Adapter is INACTIVE when the
9
+ project has no ``.github/`` directory — we do not create it ourselves.
10
+
11
+ Hard rules covered here:
12
+ - A1 / A2 / A3 / A7: via ``adapter_base.atomic_write``.
13
+ - A4: soft 2 KB + hard 4 KB cap enforcement.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import os
20
+ from pathlib import Path
21
+
22
+ from superlocalmemory.core.security_primitives import (
23
+ PathTraversalError,
24
+ safe_resolve,
25
+ )
26
+ from superlocalmemory.hooks.adapter_base import (
27
+ COPILOT_SOFT_BYTES,
28
+ HARD_BYTES_CAP,
29
+ WriteResult,
30
+ atomic_write,
31
+ record_disable,
32
+ truncate_to_cap,
33
+ )
34
+ from superlocalmemory.hooks.context_payload import (
35
+ ContextPayload,
36
+ RecallFn,
37
+ build_payload,
38
+ format_entities,
39
+ format_topics,
40
+ truncate_payload_for_cap,
41
+ )
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ NAME = "copilot_project"
46
+ TARGET_REL = ".github/copilot-instructions.md"
47
+
48
+ _BODY_TEMPLATE = (
49
+ "# SLM Active Brain Context (auto-generated)\n\n"
50
+ "_Regenerated by SuperLocalMemory every 15 min. Version {version}._\n\n"
51
+ "## User preferences\n{topics}\n\n"
52
+ "## Entities\n{entities}\n\n"
53
+ "## Never do\n"
54
+ "- Do not modify files under `.slm/`\n"
55
+ "- Do not commit `*.slm-cache.db`\n"
56
+ )
57
+
58
+
59
+ def render_copilot(payload: ContextPayload) -> bytes:
60
+ return _BODY_TEMPLATE.format(
61
+ version=payload.version,
62
+ topics=format_topics(payload),
63
+ entities=format_entities(payload),
64
+ ).encode("utf-8")
65
+
66
+
67
+ class CopilotAdapter:
68
+ """Project-scope Copilot adapter."""
69
+
70
+ def __init__(
71
+ self,
72
+ *,
73
+ base_dir: Path,
74
+ sync_log_db: Path,
75
+ recall_fn: RecallFn,
76
+ profile_id: str = "default",
77
+ soft_cap: int = COPILOT_SOFT_BYTES,
78
+ hard_cap: int = HARD_BYTES_CAP,
79
+ ) -> None:
80
+ self._base_dir = Path(base_dir)
81
+ self._sync_log_db = Path(sync_log_db)
82
+ self._recall_fn = recall_fn
83
+ self._profile_id = profile_id
84
+ self._soft_cap = soft_cap
85
+ self._hard_cap = hard_cap
86
+ self.name = NAME
87
+ self._inactive_until_retry = False
88
+
89
+ @property
90
+ def target_path(self) -> Path:
91
+ return safe_resolve(self._base_dir, TARGET_REL)
92
+
93
+ def is_active(self) -> bool:
94
+ if os.environ.get("SLM_COPILOT_DISABLED") == "1":
95
+ return False
96
+ if self._inactive_until_retry:
97
+ return False
98
+ if os.environ.get("SLM_COPILOT_FORCE") == "1":
99
+ return True
100
+ # Active iff `.github/` directory already exists in the project.
101
+ github_dir = self._base_dir / ".github"
102
+ return github_dir.is_dir()
103
+
104
+ def sync(self) -> bool:
105
+ try:
106
+ resolved = self.target_path
107
+ except PathTraversalError:
108
+ logger.warning("copilot: path-traversal refused")
109
+ self._inactive_until_retry = True
110
+ return False
111
+
112
+ payload = build_payload(
113
+ self._profile_id, "project", self._base_dir,
114
+ recall_fn=self._recall_fn,
115
+ )
116
+
117
+ # Prefer the soft cap; fall back to hard cap.
118
+ rendered = truncate_payload_for_cap(
119
+ payload, hard_cap=self._soft_cap, render=render_copilot,
120
+ )
121
+ if len(rendered) > self._hard_cap:
122
+ rendered = truncate_payload_for_cap(
123
+ payload, hard_cap=self._hard_cap, render=render_copilot,
124
+ )
125
+ rendered = truncate_to_cap(rendered, cap=self._hard_cap)
126
+
127
+ result: WriteResult = atomic_write(
128
+ resolved, rendered,
129
+ adapter_name=self.name,
130
+ profile_id=self._profile_id,
131
+ sync_log_db=self._sync_log_db,
132
+ )
133
+ return result.wrote
134
+
135
+ def disable(self) -> None:
136
+ try:
137
+ resolved = self.target_path
138
+ except PathTraversalError:
139
+ return
140
+ if resolved.exists():
141
+ try:
142
+ resolved.unlink()
143
+ except OSError: # pragma: no cover
144
+ pass
145
+ record_disable(
146
+ resolved,
147
+ adapter_name=self.name,
148
+ profile_id=self._profile_id,
149
+ sync_log_db=self._sync_log_db,
150
+ )
151
+ self._inactive_until_retry = True
152
+
153
+
154
+ __all__ = ("CopilotAdapter", "NAME", "TARGET_REL", "render_copilot")
@@ -0,0 +1,90 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.22 — LLD-05 §8.2
4
+
5
+ """Cross-platform adapter orchestrator (``slm connect``).
6
+
7
+ LLD-05 §8.2. Holds the active set of adapters, detects which are installed,
8
+ runs a one-shot sync over every active adapter, and flips an individual
9
+ adapter off. Kept in its own module so it can be covered in isolation from
10
+ the legacy ``IDEConnector`` shim that still lives in ``ide_connector.py``
11
+ for backward compatibility with existing SLM users.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from dataclasses import dataclass
18
+ from typing import Iterable
19
+
20
+ from superlocalmemory.hooks.adapter_base import Adapter
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class AdapterStatus:
27
+ """Return record for ``CrossPlatformConnector.detect``."""
28
+ name: str
29
+ active: bool
30
+ target_path: str
31
+
32
+
33
+ class CrossPlatformConnector:
34
+ """LLD-05 §8.2 orchestrator.
35
+
36
+ A thin coordinator: every adapter knows how to detect itself, sync
37
+ itself, and disable itself. This class just iterates, catches, and
38
+ reports. Errors never abort the iteration (A8).
39
+ """
40
+
41
+ def __init__(self, adapters: Iterable[Adapter]) -> None:
42
+ self._adapters: list[Adapter] = list(adapters)
43
+
44
+ @property
45
+ def adapters(self) -> list[Adapter]:
46
+ return list(self._adapters)
47
+
48
+ def detect(self) -> list[AdapterStatus]:
49
+ out: list[AdapterStatus] = []
50
+ for a in self._adapters:
51
+ try:
52
+ active = a.is_active()
53
+ except Exception:
54
+ active = False
55
+ try:
56
+ target = str(a.target_path)
57
+ except Exception:
58
+ target = "?"
59
+ out.append(AdapterStatus(name=a.name, active=active,
60
+ target_path=target))
61
+ return out
62
+
63
+ def connect(self) -> dict[str, str]:
64
+ """One-shot sync over every active adapter."""
65
+ results: dict[str, str] = {}
66
+ for a in self._adapters:
67
+ try:
68
+ if not a.is_active():
69
+ results[a.name] = "inactive"
70
+ continue
71
+ wrote = a.sync()
72
+ results[a.name] = "wrote" if wrote else "skipped"
73
+ except Exception as exc:
74
+ logger.warning("adapter %s failed: %s", a.name, exc)
75
+ results[a.name] = f"error:{type(exc).__name__}"
76
+ return results
77
+
78
+ def disable(self, name: str) -> bool:
79
+ for a in self._adapters:
80
+ if a.name == name:
81
+ try:
82
+ a.disable()
83
+ except Exception as exc:
84
+ logger.warning("disable %s failed: %s", name, exc)
85
+ return False
86
+ return True
87
+ return False
88
+
89
+
90
+ __all__ = ("AdapterStatus", "CrossPlatformConnector")