superlocalmemory 3.7.8 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +129 -0
- package/README.md +205 -123
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +2 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +228 -17
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +187 -0
- package/src/superlocalmemory/cli/main.py +49 -8
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +21 -7
- package/src/superlocalmemory/core/embeddings.py +131 -46
- package/src/superlocalmemory/core/engine.py +41 -22
- package/src/superlocalmemory/core/engine_ingestion.py +359 -43
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -3
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +186 -29
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +148 -3
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +263 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +32 -79
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +12 -4
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +221 -47
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +317 -70
- package/src/superlocalmemory/server/routes/behavioral.py +349 -71
- package/src/superlocalmemory/server/routes/brain.py +69 -12
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +438 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +108 -26
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +189 -68
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +289 -118
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +200 -31
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +103 -158
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +247 -89
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +827 -72
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +132 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +136 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +303 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +100 -41
- package/src/superlocalmemory/ui/js/dashboard.js +403 -65
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +871 -0
- package/src/superlocalmemory/ui/js/od-brain.js +816 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +600 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +929 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
- package/src/superlocalmemory/ui/js/od-shell.js +809 -0
- package/src/superlocalmemory/ui/js/od-skills.js +600 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +17 -3
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -23
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -16
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -38,7 +38,11 @@ from typing import Protocol, runtime_checkable
|
|
|
38
38
|
# ---------------------------------------------------------------------------
|
|
39
39
|
|
|
40
40
|
HARD_BYTES_CAP = 4096
|
|
41
|
-
|
|
41
|
+
# Soft budget for the managed instruction block. Raised 2048 -> 2560 -> 2816 as
|
|
42
|
+
# the block grew to carry the memory, token-optimization, and (compact)
|
|
43
|
+
# bounded-loop protocols; the 4 KB hard cap still bounds total size (recall
|
|
44
|
+
# content is truncated to stay under it).
|
|
45
|
+
COPILOT_SOFT_BYTES = 2816
|
|
42
46
|
TRUNCATION_MARKER = b"\n<!-- truncated -->"
|
|
43
47
|
|
|
44
48
|
|
|
@@ -10,6 +10,7 @@ import logging
|
|
|
10
10
|
from typing import Any, Callable
|
|
11
11
|
|
|
12
12
|
from superlocalmemory.core.injection import InjectableMemory, render_context
|
|
13
|
+
from superlocalmemory.retrieval.temporal_frame import relative_age, temporal_frame
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
@@ -85,7 +86,13 @@ class AutoRecall:
|
|
|
85
86
|
)
|
|
86
87
|
for r in relevant[:self._max_memories]
|
|
87
88
|
]
|
|
88
|
-
|
|
89
|
+
ctx = render_context(memories, mode="B", cfg=None, wrap=True)
|
|
90
|
+
# T-inject: anchor the injected memories to "now" so a time-blind
|
|
91
|
+
# model can weigh recency. Prepend a one-line temporal frame.
|
|
92
|
+
frame = temporal_frame(
|
|
93
|
+
[getattr(r.fact, "created_at", "") for r in relevant[:self._max_memories]]
|
|
94
|
+
)
|
|
95
|
+
return f"{frame}\n\n{ctx}" if ctx else ctx
|
|
89
96
|
except Exception as exc:
|
|
90
97
|
logger.warning("Auto-recall failed: %s", exc)
|
|
91
98
|
return ""
|
|
@@ -104,12 +111,17 @@ class AutoRecall:
|
|
|
104
111
|
response = self._recall(query, self._max_memories)
|
|
105
112
|
if response is None:
|
|
106
113
|
return []
|
|
114
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
115
|
+
_now = _dt.now(_tz.utc)
|
|
107
116
|
results = []
|
|
108
117
|
for r in response.results:
|
|
109
118
|
if r.score >= self._threshold:
|
|
119
|
+
_created = getattr(r.fact, "created_at", "") or ""
|
|
110
120
|
results.append({
|
|
111
121
|
"fact_id": r.fact.fact_id,
|
|
112
122
|
"content": r.fact.content[:300],
|
|
123
|
+
"created_at": _created,
|
|
124
|
+
"age_label": relative_age(_created, _now),
|
|
113
125
|
"score": round(r.score, 3),
|
|
114
126
|
"relevance_score": round(
|
|
115
127
|
getattr(r, "relevance_score", r.score) or 0.0, 3
|
|
@@ -261,6 +261,17 @@ def _hook_definitions(include_gate: bool = False) -> dict[str, list]:
|
|
|
261
261
|
"command": _wrap_python_cmd("stop_outcome"),
|
|
262
262
|
"timeout": 10000,
|
|
263
263
|
},
|
|
264
|
+
# Commit temporal summaries so session decisions survive beyond
|
|
265
|
+
# the git-state snapshot written by `slm hook stop`.
|
|
266
|
+
{
|
|
267
|
+
"type": "command",
|
|
268
|
+
"command": (
|
|
269
|
+
'cmd /c "slm session close 2>NUL || exit /b 0"'
|
|
270
|
+
if sys.platform == "win32"
|
|
271
|
+
else "slm session close 2>/dev/null || true"
|
|
272
|
+
),
|
|
273
|
+
"timeout": 15000,
|
|
274
|
+
},
|
|
264
275
|
]
|
|
265
276
|
}
|
|
266
277
|
],
|
|
@@ -7,10 +7,16 @@ import sysconfig
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
SKILLS = ("slm-cache", "slm-compress", "slm-graph", "slm-recall", "slm-remember", "slm-session", "slm-status")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
|
|
11
|
+
# Codex subagent files written to ~/.codex/agents (content built by _agent_files()).
|
|
12
|
+
AGENTS = ("slm-memory-advisor.toml", "slm-optimize-advisor.toml")
|
|
13
|
+
|
|
14
|
+
_MEMORY_ADVISOR_TOML = (
|
|
15
|
+
'name = "slm-memory-advisor"\n'
|
|
16
|
+
'description = "Use SuperLocalMemory safely: initialize once, recall before remember, and store only durable atomic facts."\n'
|
|
17
|
+
'instructions = "Use SLM for memory discipline only. Check results before claiming success; preserve private scope unless the user explicitly asks to share."\n'
|
|
18
|
+
)
|
|
19
|
+
|
|
14
20
|
|
|
15
21
|
def _source_root() -> Path:
|
|
16
22
|
development = Path(__file__).resolve().parents[3] / "plugin-src" / "skills"
|
|
@@ -21,6 +27,57 @@ def _source_root() -> Path:
|
|
|
21
27
|
return installed
|
|
22
28
|
raise FileNotFoundError("Bundled Codex skills were not found in this installation")
|
|
23
29
|
|
|
30
|
+
|
|
31
|
+
def _agents_source_root() -> Path | None:
|
|
32
|
+
development = Path(__file__).resolve().parents[3] / "plugin-src" / "agents"
|
|
33
|
+
if development.exists():
|
|
34
|
+
return development
|
|
35
|
+
installed = Path(sysconfig.get_path("data")) / "share" / "superlocalmemory" / "codex" / "agents"
|
|
36
|
+
return installed if installed.exists() else None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _optimize_advisor_toml() -> str:
|
|
40
|
+
"""Build the optimize-advisor TOML from the canonical advisor doc so Codex
|
|
41
|
+
ships the FULL decision rules (the 8-rule tree), not a one-line stub. Falls
|
|
42
|
+
back to a short instruction only if the source doc is unavailable.
|
|
43
|
+
"""
|
|
44
|
+
description = (
|
|
45
|
+
"Apply SuperLocalMemory's no-proxy context-optimization rules — reversible "
|
|
46
|
+
"compression of large tool output and KV-caching of repeated reads/searches."
|
|
47
|
+
)
|
|
48
|
+
body = ""
|
|
49
|
+
root = _agents_source_root()
|
|
50
|
+
if root is not None:
|
|
51
|
+
src = root / "slm-optimize-advisor.md"
|
|
52
|
+
if src.exists():
|
|
53
|
+
text = src.read_text(encoding="utf-8")
|
|
54
|
+
if text.startswith("---"): # strip YAML frontmatter, keep the guidance body
|
|
55
|
+
end = text.find("\n---", 3)
|
|
56
|
+
if end != -1:
|
|
57
|
+
text = text[end + 4:]
|
|
58
|
+
body = text.strip()
|
|
59
|
+
if not body:
|
|
60
|
+
body = (
|
|
61
|
+
"Reduce context-window pressure with the Surface-B tools (reversible CCR "
|
|
62
|
+
"compression + a per-agent KV cache); fail-open — never block the task."
|
|
63
|
+
)
|
|
64
|
+
# TOML literal multi-line string ('''...'''): no escape processing, and the
|
|
65
|
+
# advisor body contains no ''' sequence.
|
|
66
|
+
return (
|
|
67
|
+
'name = "slm-optimize-advisor"\n'
|
|
68
|
+
f'description = "{description}"\n'
|
|
69
|
+
f"instructions = '''\n{body}\n'''\n"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _agent_files() -> dict:
|
|
74
|
+
"""Return {filename: TOML content} for the Codex subagents."""
|
|
75
|
+
return {
|
|
76
|
+
"slm-memory-advisor.toml": _MEMORY_ADVISOR_TOML,
|
|
77
|
+
"slm-optimize-advisor.toml": _optimize_advisor_toml(),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
24
81
|
def install_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
|
|
25
82
|
"""Copy only named SLM assets; never rewrite user-owned assets."""
|
|
26
83
|
home = home or Path.home()
|
|
@@ -37,10 +94,11 @@ def install_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
|
|
|
37
94
|
target = skills_root / skill
|
|
38
95
|
target.mkdir(parents=True, exist_ok=True)
|
|
39
96
|
shutil.copy2(source / skill / "SKILL.md", target / "SKILL.md")
|
|
40
|
-
for filename, content in
|
|
97
|
+
for filename, content in _agent_files().items():
|
|
41
98
|
(agents_root / filename).write_text(content, encoding="utf-8")
|
|
42
99
|
return {"success": True, "skills": list(SKILLS), "agents": list(AGENTS), "dry_run": False}
|
|
43
100
|
|
|
101
|
+
|
|
44
102
|
def remove_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
|
|
45
103
|
"""Remove only the known SLM directories and files."""
|
|
46
104
|
home = home or Path.home()
|
|
@@ -52,6 +110,7 @@ def remove_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
|
|
|
52
110
|
shutil.rmtree(target) if target.is_dir() else target.unlink()
|
|
53
111
|
return {"success": True, "removed": [str(x) for x in existing], "dry_run": dry_run}
|
|
54
112
|
|
|
113
|
+
|
|
55
114
|
def status_assets(*, home: Path | None = None) -> dict:
|
|
56
115
|
home = home or Path.home()
|
|
57
116
|
skills = [x for x in SKILLS if (home / ".agents" / "skills" / x / "SKILL.md").exists()]
|
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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()
|
|
@@ -26,6 +26,8 @@ import urllib.error
|
|
|
26
26
|
import urllib.request
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
|
+
from superlocalmemory import __version__
|
|
30
|
+
|
|
29
31
|
# ---------------------------------------------------------------------------
|
|
30
32
|
# Cross-platform temp paths
|
|
31
33
|
# ---------------------------------------------------------------------------
|
|
@@ -221,7 +223,10 @@ def _codex_mcp_session_init(project_dir: str, payload: dict) -> dict:
|
|
|
221
223
|
proc.stdin.write(json.dumps({
|
|
222
224
|
"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
223
225
|
"params": {"protocolVersion": "2024-11-05", "capabilities": {},
|
|
224
|
-
"clientInfo": {
|
|
226
|
+
"clientInfo": {
|
|
227
|
+
"name": "superlocalmemory-codex-hook",
|
|
228
|
+
"version": __version__,
|
|
229
|
+
}},
|
|
225
230
|
}) + "\n")
|
|
226
231
|
proc.stdin.flush()
|
|
227
232
|
proc.stdout.readline()
|
|
@@ -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=
|
|
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
|
),
|
|
@@ -240,11 +240,12 @@ def connect_ide(
|
|
|
240
240
|
here: bool = False,
|
|
241
241
|
profile: str | None = None,
|
|
242
242
|
agents_md_source: Callable[[], str] | None = None,
|
|
243
|
+
dry_run: bool = False,
|
|
243
244
|
) -> dict[str, Any]:
|
|
244
245
|
"""Wire SLM into the target IDE config via merge-not-clobber.
|
|
245
246
|
|
|
246
247
|
Returns a result dict:
|
|
247
|
-
{ide, mcp_config: wrote|merged|unchanged|skipped|error,
|
|
248
|
+
{ide, mcp_config: wrote|merged|unchanged|would_write|skipped|error,
|
|
248
249
|
mcp_path, agents_md: wrote|skipped(...)|unchanged|error,
|
|
249
250
|
servers_preserved: int, error: str|None}
|
|
250
251
|
"""
|
|
@@ -265,6 +266,13 @@ def connect_ide(
|
|
|
265
266
|
)
|
|
266
267
|
return result
|
|
267
268
|
|
|
269
|
+
if ide_id == "vscode-copilot" and not here:
|
|
270
|
+
result["error"] = (
|
|
271
|
+
"VS Code / Copilot integration is project-scoped. "
|
|
272
|
+
"Run `slm connect vscode-copilot --here` from the project root."
|
|
273
|
+
)
|
|
274
|
+
return result
|
|
275
|
+
|
|
268
276
|
# Step 1a — claude-code short-circuit (AC6)
|
|
269
277
|
if desc.fmt == "":
|
|
270
278
|
print(CLAUDE_CODE_PLUGIN_POINTER)
|
|
@@ -325,7 +333,28 @@ def connect_ide(
|
|
|
325
333
|
result["servers_preserved"] = max(0, pre_count - (0 if pre_slm is None else 1))
|
|
326
334
|
result["mcp_config"] = mcp_status
|
|
327
335
|
|
|
328
|
-
# Step 6 — atomic write
|
|
336
|
+
# Step 6 — atomic write. A dry run deliberately exercises the merge and
|
|
337
|
+
# packaged-asset lookup path without touching a user-owned IDE config.
|
|
338
|
+
if dry_run:
|
|
339
|
+
result["mcp_config"] = "would_write"
|
|
340
|
+
if desc.agents_md_path is None:
|
|
341
|
+
result["agents_md"] = "skipped(unsupported)"
|
|
342
|
+
elif agents_md_source is None:
|
|
343
|
+
result["agents_md"] = "skipped(no-source)"
|
|
344
|
+
else:
|
|
345
|
+
try:
|
|
346
|
+
source_content = agents_md_source()
|
|
347
|
+
except Exception as exc:
|
|
348
|
+
result["agents_md"] = "error(source-unavailable)"
|
|
349
|
+
result["error"] = f"AGENTS.md asset lookup failed: {exc}"
|
|
350
|
+
return result
|
|
351
|
+
if not isinstance(source_content, str) or not source_content.strip():
|
|
352
|
+
result["agents_md"] = "error(empty-source)"
|
|
353
|
+
result["error"] = "AGENTS.md asset is empty"
|
|
354
|
+
return result
|
|
355
|
+
result["agents_md"] = "would_write"
|
|
356
|
+
return result
|
|
357
|
+
|
|
329
358
|
try:
|
|
330
359
|
_atomic_write(config_path, data, desc.fmt)
|
|
331
360
|
except Exception as exc:
|
|
@@ -347,6 +376,54 @@ def connect_ide(
|
|
|
347
376
|
return result
|
|
348
377
|
|
|
349
378
|
|
|
379
|
+
def connect_many(
|
|
380
|
+
ide_ids: list[str],
|
|
381
|
+
*,
|
|
382
|
+
home: Path | None = None,
|
|
383
|
+
project: Path | None = None,
|
|
384
|
+
here: bool = False,
|
|
385
|
+
profile: str | None = None,
|
|
386
|
+
agents_md_source: Callable[[], str] | None = None,
|
|
387
|
+
) -> list[dict[str, Any]]:
|
|
388
|
+
"""Wire SLM into multiple IDE configs via non-destructive merge.
|
|
389
|
+
|
|
390
|
+
Iterates over ``ide_ids`` and calls :func:`connect_ide` for each entry.
|
|
391
|
+
Each IDE is processed independently — a failure on one IDE does NOT abort
|
|
392
|
+
the remaining targets.
|
|
393
|
+
|
|
394
|
+
The underlying :func:`connect_ide` is MERGE-NOT-CLOBBER:
|
|
395
|
+
- Only the ``superlocalmemory`` server key is touched.
|
|
396
|
+
- All other MCP servers + top-level keys are preserved byte-for-byte.
|
|
397
|
+
- Writes are atomic (.tmp + os.replace).
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
ide_ids: IDE ids to wire (from :data:`IDE_MATRIX`). Pass an empty
|
|
401
|
+
list to no-op. Unknown ids produce error entries in the output.
|
|
402
|
+
home: Override ``$HOME`` (test hook).
|
|
403
|
+
project: Project root for ``here=True`` installs.
|
|
404
|
+
here: When True, write to project-relative path instead of global.
|
|
405
|
+
profile: Inject ``SLM_MCP_PROFILE`` env-var into every server block.
|
|
406
|
+
agents_md_source: Callable returning AGENTS.md content to append.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
List of per-IDE result dicts, one per input id. Each dict has the
|
|
410
|
+
same shape as :func:`connect_ide`'s return value::
|
|
411
|
+
|
|
412
|
+
{ide, mcp_config, mcp_path, agents_md, servers_preserved, error}
|
|
413
|
+
"""
|
|
414
|
+
return [
|
|
415
|
+
connect_ide(
|
|
416
|
+
ide_id,
|
|
417
|
+
home=home,
|
|
418
|
+
project=project,
|
|
419
|
+
here=here,
|
|
420
|
+
profile=profile,
|
|
421
|
+
agents_md_source=agents_md_source,
|
|
422
|
+
)
|
|
423
|
+
for ide_id in ide_ids
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
|
|
350
427
|
# ---------------------------------------------------------------------------
|
|
351
428
|
# Internal helpers
|
|
352
429
|
# ---------------------------------------------------------------------------
|
|
@@ -480,6 +557,9 @@ def _handle_agents_md(
|
|
|
480
557
|
except Exception as exc:
|
|
481
558
|
logger.warning("agents_md_source() failed: %s — skipping AGENTS.md write", exc)
|
|
482
559
|
return "skipped(source-error)"
|
|
560
|
+
if not isinstance(source_content, str) or not source_content.strip():
|
|
561
|
+
logger.warning("agents_md_source() returned no content — skipping AGENTS.md write")
|
|
562
|
+
return "skipped(no-source-content)"
|
|
483
563
|
|
|
484
564
|
# Read existing content
|
|
485
565
|
existing = ""
|
|
@@ -504,3 +584,68 @@ def _handle_agents_md(
|
|
|
504
584
|
_tmp.write_text(existing + section, encoding="utf-8")
|
|
505
585
|
os.replace(_tmp, agents_path)
|
|
506
586
|
return "wrote"
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# ---------------------------------------------------------------------------
|
|
590
|
+
# __main__ — `python -m superlocalmemory.hooks.portable_kit [ids...]`
|
|
591
|
+
#
|
|
592
|
+
# Used by the npm installer to execute non-destructive IDE connects when
|
|
593
|
+
# Python is available at postinstall time. If Python / the package is not
|
|
594
|
+
# importable the installer records a pending_ide_connections.json instead.
|
|
595
|
+
# ---------------------------------------------------------------------------
|
|
596
|
+
|
|
597
|
+
if __name__ == "__main__": # pragma: no cover
|
|
598
|
+
import argparse as _argparse
|
|
599
|
+
|
|
600
|
+
_parser = _argparse.ArgumentParser(
|
|
601
|
+
description="SLM IDE connector — non-destructive merge-not-clobber."
|
|
602
|
+
)
|
|
603
|
+
_parser.add_argument(
|
|
604
|
+
"ide_ids",
|
|
605
|
+
nargs="*",
|
|
606
|
+
metavar="IDE",
|
|
607
|
+
help=(
|
|
608
|
+
"IDE ids to connect. Pass 'all' to connect every supported IDE "
|
|
609
|
+
"(excluding claude-code which uses the WP-06 plugin)."
|
|
610
|
+
),
|
|
611
|
+
)
|
|
612
|
+
_parser.add_argument("--home", help="Override home directory (test hook).")
|
|
613
|
+
_parser.add_argument("--profile", help="SLM_MCP_PROFILE to inject.")
|
|
614
|
+
_parser.add_argument(
|
|
615
|
+
"--list", action="store_true",
|
|
616
|
+
help="Print supported IDE ids with display names and exit.",
|
|
617
|
+
)
|
|
618
|
+
_args = _parser.parse_args()
|
|
619
|
+
|
|
620
|
+
if _args.list:
|
|
621
|
+
for _id, _desc in IDE_MATRIX.items():
|
|
622
|
+
_flag = "[OUT]" if not _desc.fmt else ""
|
|
623
|
+
print(f" {_id:22s} {_desc.display} {_flag}".rstrip())
|
|
624
|
+
sys.exit(0)
|
|
625
|
+
|
|
626
|
+
_home = Path(_args.home) if _args.home else None
|
|
627
|
+
|
|
628
|
+
# Resolve targets: "all" → every MCP-capable IDE (fmt != ""), else explicit list.
|
|
629
|
+
if "all" in _args.ide_ids:
|
|
630
|
+
_targets = [k for k, d in IDE_MATRIX.items() if d.fmt]
|
|
631
|
+
else:
|
|
632
|
+
_targets = [t for t in _args.ide_ids if t]
|
|
633
|
+
|
|
634
|
+
if not _targets:
|
|
635
|
+
print("No IDE ids given. Pass ide ids or 'all'. Use --list to see options.")
|
|
636
|
+
sys.exit(0)
|
|
637
|
+
|
|
638
|
+
_results = connect_many(_targets, home=_home, profile=_args.profile)
|
|
639
|
+
_ok = 0
|
|
640
|
+
_fail = 0
|
|
641
|
+
for _r in _results:
|
|
642
|
+
_err = _r.get("error")
|
|
643
|
+
_status = _r.get("mcp_config", "error")
|
|
644
|
+
if _err:
|
|
645
|
+
print(f" ERROR {_r['ide']}: {_err}", file=sys.stderr)
|
|
646
|
+
_fail += 1
|
|
647
|
+
else:
|
|
648
|
+
print(f" {_status.upper():9s} {_r['ide']} → {_r.get('mcp_path', '')}")
|
|
649
|
+
_ok += 1
|
|
650
|
+
print(f"\n{_ok} connected, {_fail} errors.")
|
|
651
|
+
sys.exit(0 if _fail == 0 else 1)
|
|
@@ -266,7 +266,18 @@ class BackupManager:
|
|
|
266
266
|
|
|
267
267
|
A safety snapshot of the current state is taken first.
|
|
268
268
|
"""
|
|
269
|
-
|
|
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
|
-
|
|
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
|