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.
- package/CHANGELOG.md +24 -0
- package/README.md +42 -34
- package/bin/slm +11 -0
- package/bin/slm.bat +12 -0
- package/package.json +4 -3
- package/pyproject.toml +4 -3
- package/scripts/build-slm-hook.ps1 +40 -0
- package/scripts/build-slm-hook.sh +45 -0
- package/scripts/build_entry.py +452 -0
- package/scripts/ci/stage5b_gate.sh +50 -0
- package/scripts/postinstall/validation.js +187 -0
- package/scripts/postinstall-interactive.js +756 -0
- package/scripts/postinstall_binary.js +287 -0
- package/scripts/release_manifest.py +273 -0
- package/scripts/slm-hook.spec +56 -0
- package/skills/slm-build-graph/SKILL.md +423 -0
- package/skills/slm-list-recent/SKILL.md +348 -0
- package/skills/slm-recall/SKILL.md +343 -0
- package/skills/slm-remember/SKILL.md +194 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/skills/slm-status/SKILL.md +363 -0
- package/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/cli/commands.py +254 -79
- package/src/superlocalmemory/cli/context_commands.py +192 -0
- package/src/superlocalmemory/cli/daemon.py +15 -1
- package/src/superlocalmemory/cli/db_migrate.py +80 -0
- package/src/superlocalmemory/cli/escape_hatch.py +220 -0
- package/src/superlocalmemory/cli/main.py +72 -1
- package/src/superlocalmemory/core/context_cache.py +397 -0
- package/src/superlocalmemory/core/engine.py +38 -2
- package/src/superlocalmemory/core/engine_wiring.py +1 -1
- package/src/superlocalmemory/core/ram_lock.py +111 -0
- package/src/superlocalmemory/core/recall_pipeline.py +433 -3
- package/src/superlocalmemory/core/recall_worker.py +8 -3
- package/src/superlocalmemory/core/security_primitives.py +635 -0
- package/src/superlocalmemory/core/shadow_router.py +319 -0
- package/src/superlocalmemory/core/slm_disabled.py +87 -0
- package/src/superlocalmemory/core/slmignore.py +125 -0
- package/src/superlocalmemory/core/topic_signature.py +143 -0
- package/src/superlocalmemory/core/worker_pool.py +14 -3
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
- package/src/superlocalmemory/evolution/budget.py +321 -0
- package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
- package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
- package/src/superlocalmemory/hooks/adapter_base.py +317 -0
- package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
- package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
- package/src/superlocalmemory/hooks/context_payload.py +312 -0
- package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
- package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
- package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
- package/src/superlocalmemory/hooks/ide_connector.py +25 -2
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
- package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
- package/src/superlocalmemory/hooks/session_registry.py +186 -0
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
- package/src/superlocalmemory/hooks/sync_loop.py +114 -0
- package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
- package/src/superlocalmemory/infra/backup.py +3 -3
- package/src/superlocalmemory/infra/cloud_backup.py +2 -2
- package/src/superlocalmemory/infra/event_bus.py +2 -2
- package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
- package/src/superlocalmemory/learning/arm_catalog.py +99 -0
- package/src/superlocalmemory/learning/bandit.py +526 -0
- package/src/superlocalmemory/learning/bandit_cache.py +133 -0
- package/src/superlocalmemory/learning/behavioral.py +53 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
- package/src/superlocalmemory/learning/database.py +256 -0
- package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
- package/src/superlocalmemory/learning/ensemble.py +300 -0
- package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
- package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
- package/src/superlocalmemory/learning/labeler.py +87 -0
- package/src/superlocalmemory/learning/legacy_migration.py +277 -0
- package/src/superlocalmemory/learning/memory_merge.py +160 -0
- package/src/superlocalmemory/learning/model_cache.py +269 -0
- package/src/superlocalmemory/learning/model_rollback.py +278 -0
- package/src/superlocalmemory/learning/outcome_queue.py +284 -0
- package/src/superlocalmemory/learning/pattern_miner.py +415 -0
- package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
- package/src/superlocalmemory/learning/ranker.py +225 -81
- package/src/superlocalmemory/learning/ranker_common.py +163 -0
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
- package/src/superlocalmemory/learning/reward.py +777 -0
- package/src/superlocalmemory/learning/reward_archive.py +210 -0
- package/src/superlocalmemory/learning/reward_boost.py +201 -0
- package/src/superlocalmemory/learning/reward_proxy.py +326 -0
- package/src/superlocalmemory/learning/shadow_test.py +524 -0
- package/src/superlocalmemory/learning/signal_worker.py +270 -0
- package/src/superlocalmemory/learning/signals.py +314 -0
- package/src/superlocalmemory/learning/trigram_index.py +547 -0
- package/src/superlocalmemory/mcp/server.py +5 -5
- package/src/superlocalmemory/mcp/tools_context.py +183 -0
- package/src/superlocalmemory/mcp/tools_core.py +92 -27
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
- package/src/superlocalmemory/retrieval/engine.py +52 -0
- package/src/superlocalmemory/server/api.py +2 -2
- package/src/superlocalmemory/server/bandit_loops.py +140 -0
- package/src/superlocalmemory/server/middleware/__init__.py +11 -0
- package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
- package/src/superlocalmemory/server/routes/backup.py +36 -13
- package/src/superlocalmemory/server/routes/behavioral.py +50 -19
- package/src/superlocalmemory/server/routes/brain.py +1234 -0
- package/src/superlocalmemory/server/routes/data_io.py +4 -4
- package/src/superlocalmemory/server/routes/events.py +2 -2
- package/src/superlocalmemory/server/routes/helpers.py +1 -1
- package/src/superlocalmemory/server/routes/learning.py +192 -7
- package/src/superlocalmemory/server/routes/memories.py +189 -1
- package/src/superlocalmemory/server/routes/prewarm.py +171 -0
- package/src/superlocalmemory/server/routes/profiles.py +3 -3
- package/src/superlocalmemory/server/routes/token.py +88 -0
- package/src/superlocalmemory/server/routes/ws.py +5 -5
- package/src/superlocalmemory/server/security_middleware.py +13 -7
- package/src/superlocalmemory/server/ui.py +2 -2
- package/src/superlocalmemory/server/unified_daemon.py +335 -3
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/storage/migration_runner.py +545 -0
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/ui/css/brain.css +409 -0
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
- package/src/superlocalmemory/ui/index.html +459 -1345
- package/src/superlocalmemory/ui/js/brain.js +1321 -0
- package/src/superlocalmemory/ui/js/clusters.js +123 -4
- package/src/superlocalmemory/ui/js/init.js +48 -39
- package/src/superlocalmemory/ui/js/memories.js +88 -2
- package/src/superlocalmemory/ui/js/modal.js +71 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
- package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
- package/src/superlocalmemory/ui/js/behavioral.js +0 -447
- package/src/superlocalmemory/ui/js/graph-core.js +0 -447
- package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
- package/src/superlocalmemory/ui/js/learning.js +0 -435
- package/src/superlocalmemory/ui/js/patterns.js +0 -93
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -58
- 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")
|