superlocalmemory 3.6.13 → 3.6.15
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 +17 -0
- package/CHANGELOG.md +28 -0
- package/README.md +189 -740
- package/package.json +12 -5
- package/plugin/.claude-plugin/plugin.json +20 -0
- package/plugin/.mcp.json +12 -0
- package/plugin/CLAUDE.md +44 -0
- package/plugin/_GENERATED.md +6 -0
- package/plugin/agents/slm-memory-advisor.md +44 -0
- package/plugin/agents/slm-optimize-advisor.md +38 -0
- package/plugin/hooks/hooks.json +14 -0
- package/plugin/requirements.txt +1 -0
- package/plugin/scripts/ensure-venv.bat +122 -0
- package/plugin/scripts/ensure-venv.sh +105 -0
- package/plugin/scripts/slm-launch +15 -0
- package/plugin/scripts/slm-launch.bat +17 -0
- package/plugin/settings.json +16 -0
- package/plugin/skills/slm-cache/SKILL.md +140 -0
- package/plugin/skills/slm-compress/SKILL.md +143 -0
- package/plugin/skills/slm-graph/SKILL.md +300 -0
- package/plugin/skills/slm-recall/SKILL.md +204 -0
- package/plugin/skills/slm-remember/SKILL.md +194 -0
- package/plugin/skills/slm-session/SKILL.md +207 -0
- package/plugin/skills/slm-status/SKILL.md +149 -0
- package/plugin-src/.mcp.json +12 -0
- package/plugin-src/agents/slm-memory-advisor.md +44 -0
- package/plugin-src/agents/slm-optimize-advisor.md +38 -0
- package/plugin-src/commands/slm-optimize.md +22 -0
- package/plugin-src/commands/slm-recall.md +16 -0
- package/plugin-src/commands/slm-remember.md +16 -0
- package/plugin-src/commands/slm-status.md +15 -0
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +14 -0
- package/plugin-src/manifest.json +25 -0
- package/plugin-src/requirements.txt +1 -0
- package/plugin-src/rules/AGENTS.md +91 -0
- package/plugin-src/rules/CLAUDE.md.fragment +44 -0
- package/plugin-src/scripts/ensure-venv.bat +122 -0
- package/plugin-src/scripts/ensure-venv.sh +105 -0
- package/plugin-src/scripts/slm-launch +15 -0
- package/plugin-src/scripts/slm-launch.bat +17 -0
- package/plugin-src/settings.json +16 -0
- package/plugin-src/skills/slm-cache/SKILL.md +140 -0
- package/plugin-src/skills/slm-compress/SKILL.md +143 -0
- package/plugin-src/skills/slm-graph/SKILL.md +300 -0
- package/plugin-src/skills/slm-recall/SKILL.md +204 -0
- package/plugin-src/skills/slm-remember/SKILL.md +194 -0
- package/plugin-src/skills/slm-session/SKILL.md +207 -0
- package/plugin-src/skills/slm-status/SKILL.md +149 -0
- package/pyproject.toml +6 -2
- package/scripts/__tests__/build-plugin.test.mjs +613 -0
- package/scripts/_savings_math.py +270 -0
- package/scripts/build-plugin.js +742 -0
- package/scripts/dogfood_savings.py +490 -0
- package/scripts/install-skills.ps1 +4 -334
- package/scripts/install-skills.sh +4 -435
- package/scripts/postinstall-interactive.js +0 -27
- package/scripts/postinstall.js +21 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +115 -0
- package/src/superlocalmemory/cli/commands.py +439 -41
- package/src/superlocalmemory/cli/main.py +92 -4
- package/src/superlocalmemory/cli/setup_wizard.py +47 -6
- package/src/superlocalmemory/core/backend_orchestrator.py +12 -8
- package/src/superlocalmemory/core/config.py +194 -9
- package/src/superlocalmemory/core/embeddings.py +10 -5
- package/src/superlocalmemory/core/engine.py +76 -5
- package/src/superlocalmemory/core/fact_consolidator.py +20 -3
- package/src/superlocalmemory/core/platform_utils.py +8 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/recall_worker.py +7 -0
- package/src/superlocalmemory/core/store_pipeline.py +23 -1
- package/src/superlocalmemory/core/worker_pool.py +14 -2
- package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
- package/src/superlocalmemory/hooks/portable_kit.py +506 -0
- package/src/superlocalmemory/hooks/session_registry.py +8 -4
- package/src/superlocalmemory/infra/cloud_backup.py +99 -23
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +15 -6
- package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
- package/src/superlocalmemory/mcp/server.py +75 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
- package/src/superlocalmemory/mcp/tools_core.py +37 -4
- package/src/superlocalmemory/mcp/tools_v3.py +6 -1
- package/src/superlocalmemory/mcp/tools_v33.py +8 -4
- package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
- package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
- package/src/superlocalmemory/optimize/cache/manager.py +92 -6
- package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
- package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
- package/src/superlocalmemory/optimize/compress/router.py +46 -13
- package/src/superlocalmemory/optimize/config/schema.py +6 -0
- package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
- package/src/superlocalmemory/optimize/proxy/server.py +11 -0
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
- package/src/superlocalmemory/optimize/storage/db.py +30 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +12 -2
- package/src/superlocalmemory/retrieval/engine.py +36 -3
- package/src/superlocalmemory/retrieval/entity_channel.py +5 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +10 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +10 -2
- package/src/superlocalmemory/server/recall_serializer.py +3 -1
- package/src/superlocalmemory/server/unified_daemon.py +156 -16
- package/src/superlocalmemory/storage/database.py +215 -43
- package/src/superlocalmemory/storage/migration_runner.py +17 -1
- package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +120 -0
- package/src/superlocalmemory/storage/models.py +10 -0
- package/src/superlocalmemory/storage/schema.py +15 -10
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
- package/src/superlocalmemory/ui/index.html +2 -2
- package/src/superlocalmemory/ui/js/core.js +98 -0
- package/src/superlocalmemory/ui/js/dashboard.js +8 -1
- package/src/superlocalmemory/ui/js/ide-status.js +16 -3
- package/src/superlocalmemory/ui/js/math-health.js +15 -3
- package/src/superlocalmemory/ui/js/optimize.js +18 -2
- package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +191 -741
- package/src/superlocalmemory.egg-info/SOURCES.txt +7 -9
- package/src/superlocalmemory.egg-info/requires.txt +1 -0
- package/ide/skills/slm-build-graph/SKILL.md +0 -423
- package/ide/skills/slm-list-recent/SKILL.md +0 -348
- package/ide/skills/slm-recall/SKILL.md +0 -326
- package/ide/skills/slm-remember/SKILL.md +0 -194
- package/ide/skills/slm-show-patterns/SKILL.md +0 -224
- package/ide/skills/slm-status/SKILL.md +0 -363
- package/ide/skills/slm-switch-profile/SKILL.md +0 -442
- package/skills/slm-build-graph/SKILL.md +0 -423
- package/skills/slm-list-recent/SKILL.md +0 -348
- package/skills/slm-optimize/README.md +0 -55
- package/skills/slm-optimize/SKILL.md +0 -139
- package/skills/slm-recall/SKILL.md +0 -343
- package/skills/slm-remember/SKILL.md +0 -194
- package/skills/slm-show-patterns/SKILL.md +0 -224
- package/skills/slm-status/SKILL.md +0 -363
- package/skills/slm-switch-profile/SKILL.md +0 -442
- package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
- package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +0 -442
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com
|
|
4
|
+
|
|
5
|
+
"""WP-08 portable kit — ``slm connect <ide>`` MCP-wiring.
|
|
6
|
+
|
|
7
|
+
Writes SLM's MCP block into the target IDE config via MERGE-NOT-CLOBBER:
|
|
8
|
+
- Only touches the ``superlocalmemory`` server key.
|
|
9
|
+
- All other servers + top-level keys are preserved byte-for-byte.
|
|
10
|
+
- Atomic write (.tmp + os.replace); aborts on parse error (file untouched).
|
|
11
|
+
- claude-code is OUT: short-circuits to a WP-06 plugin pointer, no config written.
|
|
12
|
+
- AGENTS.md is appended with <!-- SLM-START/END --> markers (never overwrite).
|
|
13
|
+
|
|
14
|
+
IDE_MATRIX verified against ide/configs/* templates (read-only, WP-04 owns).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import copy
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import warnings
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Callable
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Marker convention copied from ide_connector.py (do not edit that class)
|
|
32
|
+
SLM_MARKER_START = "<!-- SLM-START -->"
|
|
33
|
+
SLM_MARKER_END = "<!-- SLM-END -->"
|
|
34
|
+
|
|
35
|
+
CLAUDE_CODE_PLUGIN_POINTER = (
|
|
36
|
+
"slm connect claude-code: Claude Code is configured via the SLM plugin (WP-06).\n"
|
|
37
|
+
"Run: slm plugin install OR see plugin-src/ for manual installation.\n"
|
|
38
|
+
"No MCP config file is written by this command."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# IDEDescriptor
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class IDEDescriptor:
|
|
49
|
+
"""Immutable descriptor for one IDE in the support matrix."""
|
|
50
|
+
|
|
51
|
+
ide_id: str
|
|
52
|
+
display: str
|
|
53
|
+
mcp_path_global: str # relative to home (empty string = OUT)
|
|
54
|
+
mcp_path_project: str | None # relative to project root; None = no project scope
|
|
55
|
+
server_key: str # top-level key that holds the servers dict
|
|
56
|
+
fmt: str # "json" | "toml" | "yaml" | "" (OUT)
|
|
57
|
+
agents_md_path: str | None # relative to scope root; None = unsupported
|
|
58
|
+
server_block: dict[str, Any] = field(default_factory=dict)
|
|
59
|
+
caveats: str = ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# IDE_MATRIX — server_key + fmt VERIFIED vs ide/configs/* templates
|
|
64
|
+
# Paths are [CN-ONLINE] best-effort; confirmed from public docs where possible.
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
IDE_MATRIX: dict[str, IDEDescriptor] = {
|
|
68
|
+
# --- JSON IDEs ---
|
|
69
|
+
"cursor": IDEDescriptor(
|
|
70
|
+
ide_id="cursor",
|
|
71
|
+
display="Cursor",
|
|
72
|
+
mcp_path_global=".cursor/mcp.json",
|
|
73
|
+
mcp_path_project=".cursor/mcp.json",
|
|
74
|
+
server_key="mcpServers",
|
|
75
|
+
fmt="json",
|
|
76
|
+
agents_md_path=".cursorrules",
|
|
77
|
+
server_block={"command": "slm", "args": ["mcp"], "type": "stdio"},
|
|
78
|
+
caveats="project .cursor/mcp.json",
|
|
79
|
+
),
|
|
80
|
+
"antigravity": IDEDescriptor(
|
|
81
|
+
ide_id="antigravity",
|
|
82
|
+
display="Antigravity (agy)",
|
|
83
|
+
mcp_path_global=".antigravity/mcp.json",
|
|
84
|
+
mcp_path_project=None,
|
|
85
|
+
server_key="mcpServers",
|
|
86
|
+
fmt="json",
|
|
87
|
+
agents_md_path=None,
|
|
88
|
+
server_block={"command": "slm", "args": ["mcp"], "type": "stdio"},
|
|
89
|
+
caveats="Vertex auth (R1); path [CN-ONLINE]",
|
|
90
|
+
),
|
|
91
|
+
"windsurf": IDEDescriptor(
|
|
92
|
+
ide_id="windsurf",
|
|
93
|
+
display="Windsurf",
|
|
94
|
+
mcp_path_global=".codeium/windsurf/mcp_config.json",
|
|
95
|
+
mcp_path_project=None,
|
|
96
|
+
server_key="mcpServers",
|
|
97
|
+
fmt="json",
|
|
98
|
+
agents_md_path=".windsurfrules",
|
|
99
|
+
server_block={"command": "slm", "args": ["mcp"], "type": "stdio"},
|
|
100
|
+
caveats="path [CN-ONLINE]",
|
|
101
|
+
),
|
|
102
|
+
"gemini-cli": IDEDescriptor(
|
|
103
|
+
ide_id="gemini-cli",
|
|
104
|
+
display="Gemini CLI",
|
|
105
|
+
mcp_path_global=".gemini/settings.json",
|
|
106
|
+
mcp_path_project=None,
|
|
107
|
+
server_key="mcpServers",
|
|
108
|
+
fmt="json",
|
|
109
|
+
agents_md_path="GEMINI.md",
|
|
110
|
+
server_block={"command": "slm", "args": ["mcp"], "type": "stdio"},
|
|
111
|
+
caveats="Google deprecating; path [CN-ONLINE]",
|
|
112
|
+
),
|
|
113
|
+
"vscode-copilot": IDEDescriptor(
|
|
114
|
+
ide_id="vscode-copilot",
|
|
115
|
+
display="VS Code / Copilot",
|
|
116
|
+
mcp_path_global=".vscode/mcp.json",
|
|
117
|
+
mcp_path_project=".vscode/mcp.json",
|
|
118
|
+
server_key="servers",
|
|
119
|
+
fmt="json",
|
|
120
|
+
agents_md_path=".github/copilot-instructions.md",
|
|
121
|
+
server_block={"type": "stdio", "command": "slm", "args": ["mcp"]},
|
|
122
|
+
caveats="key NOT mcpServers; uses 'servers'",
|
|
123
|
+
),
|
|
124
|
+
"zed": IDEDescriptor(
|
|
125
|
+
ide_id="zed",
|
|
126
|
+
display="Zed Editor",
|
|
127
|
+
mcp_path_global=".config/zed/settings.json",
|
|
128
|
+
mcp_path_project=None,
|
|
129
|
+
server_key="context_servers",
|
|
130
|
+
fmt="json",
|
|
131
|
+
agents_md_path=None, # no rules surface
|
|
132
|
+
server_block={"source": "custom", "command": "slm", "args": ["mcp"]},
|
|
133
|
+
caveats="no rules surface → AGENTS.md skip",
|
|
134
|
+
),
|
|
135
|
+
"jetbrains": IDEDescriptor(
|
|
136
|
+
ide_id="jetbrains",
|
|
137
|
+
display="JetBrains IDEs",
|
|
138
|
+
mcp_path_global=".config/JetBrains/mcp.json",
|
|
139
|
+
mcp_path_project=".mcp.json",
|
|
140
|
+
server_key="mcpServers",
|
|
141
|
+
fmt="json",
|
|
142
|
+
agents_md_path=None,
|
|
143
|
+
server_block={"command": "slm", "args": ["mcp"], "type": "stdio"},
|
|
144
|
+
caveats="path per product [CN-ONLINE]",
|
|
145
|
+
),
|
|
146
|
+
"opencode": IDEDescriptor(
|
|
147
|
+
ide_id="opencode",
|
|
148
|
+
display="OpenCode",
|
|
149
|
+
mcp_path_global=".config/opencode/config.json",
|
|
150
|
+
mcp_path_project=None,
|
|
151
|
+
server_key="mcp",
|
|
152
|
+
fmt="json",
|
|
153
|
+
agents_md_path=None,
|
|
154
|
+
server_block={"command": "slm", "args": ["mcp"]},
|
|
155
|
+
caveats="top-level key is 'mcp'",
|
|
156
|
+
),
|
|
157
|
+
"claude-desktop": IDEDescriptor(
|
|
158
|
+
ide_id="claude-desktop",
|
|
159
|
+
display="Claude Desktop",
|
|
160
|
+
mcp_path_global=(
|
|
161
|
+
"Library/Application Support/Claude/claude_desktop_config.json"
|
|
162
|
+
if sys.platform == "darwin"
|
|
163
|
+
else ".config/Claude/claude_desktop_config.json"
|
|
164
|
+
),
|
|
165
|
+
mcp_path_project=None,
|
|
166
|
+
server_key="mcpServers",
|
|
167
|
+
fmt="json",
|
|
168
|
+
agents_md_path=None,
|
|
169
|
+
server_block={"command": "slm", "args": ["mcp"], "type": "stdio"},
|
|
170
|
+
caveats="desktop app (not Claude Code)",
|
|
171
|
+
),
|
|
172
|
+
# --- TOML IDEs ---
|
|
173
|
+
"codex": IDEDescriptor(
|
|
174
|
+
ide_id="codex",
|
|
175
|
+
display="Codex CLI",
|
|
176
|
+
mcp_path_global=".codex/config.toml",
|
|
177
|
+
mcp_path_project=None,
|
|
178
|
+
server_key="mcp_servers",
|
|
179
|
+
fmt="toml",
|
|
180
|
+
agents_md_path="AGENTS.md",
|
|
181
|
+
server_block={"command": "slm", "args": ["mcp"]},
|
|
182
|
+
caveats="tomllib read / tomli_w write",
|
|
183
|
+
),
|
|
184
|
+
# --- YAML IDEs ---
|
|
185
|
+
"continue": IDEDescriptor(
|
|
186
|
+
ide_id="continue",
|
|
187
|
+
display="Continue.dev",
|
|
188
|
+
mcp_path_global=".continue/config.yaml",
|
|
189
|
+
mcp_path_project=".continue/config.yaml",
|
|
190
|
+
server_key="contextProviders",
|
|
191
|
+
fmt="yaml",
|
|
192
|
+
agents_md_path=None,
|
|
193
|
+
server_block={
|
|
194
|
+
"name": "mcp",
|
|
195
|
+
"params": {
|
|
196
|
+
"serverName": "superlocalmemory",
|
|
197
|
+
"command": "slm",
|
|
198
|
+
"args": ["mcp"],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
caveats="contextProviders is a LIST; append+dedupe by serverName",
|
|
202
|
+
),
|
|
203
|
+
# --- OUT: claude-code defers to WP-06 ---
|
|
204
|
+
"claude-code": IDEDescriptor(
|
|
205
|
+
ide_id="claude-code",
|
|
206
|
+
display="Claude Code (WP-06 plugin)",
|
|
207
|
+
mcp_path_global="",
|
|
208
|
+
mcp_path_project=None,
|
|
209
|
+
server_key="",
|
|
210
|
+
fmt="",
|
|
211
|
+
agents_md_path=None,
|
|
212
|
+
server_block={},
|
|
213
|
+
caveats="OUT — WP-06 plugin pointer only; no MCP config written",
|
|
214
|
+
),
|
|
215
|
+
# --- EXPERIMENTAL (gated, not wired by default) ---
|
|
216
|
+
# chatgpt-desktop, perplexity, cody: gated behind --experimental
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Public API
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def supported_ides() -> list[str]:
|
|
226
|
+
"""Return all ide_ids in the matrix (including claude-code and experimental)."""
|
|
227
|
+
return list(IDE_MATRIX.keys())
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def resolve_descriptor(ide_id: str) -> IDEDescriptor | None:
|
|
231
|
+
"""Return the IDEDescriptor for ide_id, or None if unknown."""
|
|
232
|
+
return IDE_MATRIX.get(ide_id)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def connect_ide(
|
|
236
|
+
ide_id: str,
|
|
237
|
+
*,
|
|
238
|
+
home: Path | None = None,
|
|
239
|
+
project: Path | None = None,
|
|
240
|
+
here: bool = False,
|
|
241
|
+
profile: str | None = None,
|
|
242
|
+
agents_md_source: Callable[[], str] | None = None,
|
|
243
|
+
) -> dict[str, Any]:
|
|
244
|
+
"""Wire SLM into the target IDE config via merge-not-clobber.
|
|
245
|
+
|
|
246
|
+
Returns a result dict:
|
|
247
|
+
{ide, mcp_config: wrote|merged|unchanged|skipped|error,
|
|
248
|
+
mcp_path, agents_md: wrote|skipped(...)|unchanged|error,
|
|
249
|
+
servers_preserved: int, error: str|None}
|
|
250
|
+
"""
|
|
251
|
+
result: dict[str, Any] = {
|
|
252
|
+
"ide": ide_id,
|
|
253
|
+
"mcp_config": "error",
|
|
254
|
+
"mcp_path": "",
|
|
255
|
+
"agents_md": "skipped(not-run)",
|
|
256
|
+
"servers_preserved": 0,
|
|
257
|
+
"error": None,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Step 1 — resolve
|
|
261
|
+
desc = resolve_descriptor(ide_id)
|
|
262
|
+
if desc is None:
|
|
263
|
+
result["error"] = (
|
|
264
|
+
f"Unknown IDE '{ide_id}'. Supported: {', '.join(supported_ides())}"
|
|
265
|
+
)
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
# Step 1a — claude-code short-circuit (AC6)
|
|
269
|
+
if desc.fmt == "":
|
|
270
|
+
print(CLAUDE_CODE_PLUGIN_POINTER)
|
|
271
|
+
result["mcp_config"] = "skipped"
|
|
272
|
+
result["agents_md"] = "skipped(claude-code-out)"
|
|
273
|
+
return result
|
|
274
|
+
|
|
275
|
+
# Step 2 — scope resolution
|
|
276
|
+
effective_home = home or Path.home()
|
|
277
|
+
if here:
|
|
278
|
+
if project is None:
|
|
279
|
+
result["error"] = "--here requires --project (project root path)"
|
|
280
|
+
return result
|
|
281
|
+
scope_root = project
|
|
282
|
+
rel_path = desc.mcp_path_project or desc.mcp_path_global
|
|
283
|
+
else:
|
|
284
|
+
scope_root = effective_home
|
|
285
|
+
rel_path = desc.mcp_path_global
|
|
286
|
+
|
|
287
|
+
config_path = scope_root / rel_path
|
|
288
|
+
result["mcp_path"] = str(config_path)
|
|
289
|
+
|
|
290
|
+
# Step 3 — load existing config
|
|
291
|
+
try:
|
|
292
|
+
data = _load_config(config_path, desc.fmt)
|
|
293
|
+
except _ParseError as exc:
|
|
294
|
+
result["error"] = str(exc)
|
|
295
|
+
# File is untouched (we never wrote; abort)
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
# Step 4 — extract server container
|
|
299
|
+
# For continue (yaml list), special-case
|
|
300
|
+
if desc.fmt == "yaml":
|
|
301
|
+
mcp_status, servers_preserved = _merge_yaml_list(
|
|
302
|
+
data, desc, profile
|
|
303
|
+
)
|
|
304
|
+
result["mcp_config"] = mcp_status
|
|
305
|
+
result["servers_preserved"] = servers_preserved
|
|
306
|
+
else:
|
|
307
|
+
servers = data.setdefault(desc.server_key, {})
|
|
308
|
+
pre_count = len(servers)
|
|
309
|
+
pre_slm = copy.deepcopy(servers.get("superlocalmemory"))
|
|
310
|
+
|
|
311
|
+
# Step 5 — merge
|
|
312
|
+
block = copy.deepcopy(desc.server_block)
|
|
313
|
+
if profile:
|
|
314
|
+
block.setdefault("env", {})["SLM_MCP_PROFILE"] = profile
|
|
315
|
+
|
|
316
|
+
servers["superlocalmemory"] = block
|
|
317
|
+
|
|
318
|
+
if servers.get("superlocalmemory") == pre_slm and pre_slm is not None:
|
|
319
|
+
mcp_status = "unchanged"
|
|
320
|
+
elif pre_slm is None:
|
|
321
|
+
mcp_status = "wrote"
|
|
322
|
+
else:
|
|
323
|
+
mcp_status = "merged"
|
|
324
|
+
|
|
325
|
+
result["servers_preserved"] = max(0, pre_count - (0 if pre_slm is None else 1))
|
|
326
|
+
result["mcp_config"] = mcp_status
|
|
327
|
+
|
|
328
|
+
# Step 6 — atomic write
|
|
329
|
+
try:
|
|
330
|
+
_atomic_write(config_path, data, desc.fmt)
|
|
331
|
+
except Exception as exc:
|
|
332
|
+
result["error"] = f"Write failed: {exc}"
|
|
333
|
+
result["mcp_config"] = "error"
|
|
334
|
+
return result
|
|
335
|
+
|
|
336
|
+
# Verify idempotent: if nothing changed, re-read and confirm
|
|
337
|
+
if result["mcp_config"] != "unchanged":
|
|
338
|
+
pass # already wrote
|
|
339
|
+
else:
|
|
340
|
+
pass # already unchanged; atomic write still ran (idempotent)
|
|
341
|
+
|
|
342
|
+
# Step 7 — AGENTS.md
|
|
343
|
+
result["agents_md"] = _handle_agents_md(
|
|
344
|
+
desc, scope_root, agents_md_source, here
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
# Internal helpers
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class _ParseError(Exception):
|
|
356
|
+
"""Raised when an existing config file cannot be parsed."""
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _load_config(path: Path, fmt: str) -> dict[str, Any]:
|
|
360
|
+
"""Load and parse existing config; return {} if file absent.
|
|
361
|
+
|
|
362
|
+
Raises _ParseError if file exists but is malformed.
|
|
363
|
+
"""
|
|
364
|
+
if not path.exists():
|
|
365
|
+
return {}
|
|
366
|
+
|
|
367
|
+
raw = path.read_text(encoding="utf-8")
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
if fmt == "json":
|
|
371
|
+
return json.loads(raw)
|
|
372
|
+
elif fmt == "toml":
|
|
373
|
+
import tomllib
|
|
374
|
+
return tomllib.loads(raw)
|
|
375
|
+
elif fmt == "yaml":
|
|
376
|
+
import yaml
|
|
377
|
+
parsed = yaml.safe_load(raw)
|
|
378
|
+
# Non-dict result (e.g. bare string) is treated as empty config
|
|
379
|
+
if parsed is None:
|
|
380
|
+
return {}
|
|
381
|
+
if not isinstance(parsed, dict):
|
|
382
|
+
return {}
|
|
383
|
+
return parsed
|
|
384
|
+
else:
|
|
385
|
+
# Unknown format — return empty; caller will fail gracefully
|
|
386
|
+
return {}
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
raise _ParseError(
|
|
389
|
+
f"Config parse error ({fmt}) at {path}: {exc}"
|
|
390
|
+
) from exc
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _merge_yaml_list(
|
|
394
|
+
data: dict[str, Any],
|
|
395
|
+
desc: IDEDescriptor,
|
|
396
|
+
profile: str | None,
|
|
397
|
+
) -> tuple[str, int]:
|
|
398
|
+
"""Merge SLM entry into a list-style YAML contextProviders (continue.dev).
|
|
399
|
+
|
|
400
|
+
Returns (status, servers_preserved).
|
|
401
|
+
"""
|
|
402
|
+
providers: list[dict] = data.setdefault(desc.server_key, [])
|
|
403
|
+
if not isinstance(providers, list):
|
|
404
|
+
providers = []
|
|
405
|
+
data[desc.server_key] = providers
|
|
406
|
+
|
|
407
|
+
pre_count = sum(
|
|
408
|
+
1 for p in providers
|
|
409
|
+
if p.get("params", {}).get("serverName") != "superlocalmemory"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Check if SLM already present
|
|
413
|
+
existing_idx = None
|
|
414
|
+
for i, p in enumerate(providers):
|
|
415
|
+
if p.get("params", {}).get("serverName") == "superlocalmemory":
|
|
416
|
+
existing_idx = i
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
block = copy.deepcopy(desc.server_block)
|
|
420
|
+
if profile:
|
|
421
|
+
block.setdefault("params", {})["env"] = {"SLM_MCP_PROFILE": profile}
|
|
422
|
+
|
|
423
|
+
if existing_idx is not None:
|
|
424
|
+
if providers[existing_idx] == block:
|
|
425
|
+
return "unchanged", pre_count
|
|
426
|
+
providers[existing_idx] = block
|
|
427
|
+
return "merged", pre_count
|
|
428
|
+
else:
|
|
429
|
+
providers.append(block)
|
|
430
|
+
return "wrote", pre_count
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _atomic_write(path: Path, data: dict[str, Any], fmt: str) -> None:
|
|
434
|
+
"""Serialize data and atomically write to path (.tmp + os.replace)."""
|
|
435
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
436
|
+
tmp_path = path.with_suffix(path.suffix + ".tmp")
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
if fmt == "json":
|
|
440
|
+
content = json.dumps(data, indent=2) + "\n"
|
|
441
|
+
tmp_path.write_text(content, encoding="utf-8")
|
|
442
|
+
elif fmt == "toml":
|
|
443
|
+
import tomli_w
|
|
444
|
+
tmp_path.write_text(tomli_w.dumps(data), encoding="utf-8")
|
|
445
|
+
elif fmt == "yaml":
|
|
446
|
+
import yaml
|
|
447
|
+
tmp_path.write_text(yaml.safe_dump(data, default_flow_style=False))
|
|
448
|
+
else:
|
|
449
|
+
raise ValueError(f"Unknown format: {fmt}")
|
|
450
|
+
|
|
451
|
+
os.replace(tmp_path, path)
|
|
452
|
+
except Exception:
|
|
453
|
+
# Clean up tmp on failure
|
|
454
|
+
if tmp_path.exists():
|
|
455
|
+
tmp_path.unlink()
|
|
456
|
+
raise
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _handle_agents_md(
|
|
460
|
+
desc: IDEDescriptor,
|
|
461
|
+
scope_root: Path,
|
|
462
|
+
agents_md_source: Callable[[], str] | None,
|
|
463
|
+
here: bool,
|
|
464
|
+
) -> str:
|
|
465
|
+
"""Append SLM section to AGENTS.md with <!-- SLM-START/END --> markers.
|
|
466
|
+
|
|
467
|
+
D-1 resolution: only write AGENTS.md in --here (project) scope;
|
|
468
|
+
skip in global scope (don't litter $HOME).
|
|
469
|
+
"""
|
|
470
|
+
if desc.agents_md_path is None:
|
|
471
|
+
return "skipped(unsupported)"
|
|
472
|
+
|
|
473
|
+
if agents_md_source is None:
|
|
474
|
+
return "skipped(no-source)"
|
|
475
|
+
|
|
476
|
+
agents_path = scope_root / desc.agents_md_path
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
source_content = agents_md_source()
|
|
480
|
+
except Exception as exc:
|
|
481
|
+
logger.warning("agents_md_source() failed: %s — skipping AGENTS.md write", exc)
|
|
482
|
+
return "skipped(source-error)"
|
|
483
|
+
|
|
484
|
+
# Read existing content
|
|
485
|
+
existing = ""
|
|
486
|
+
if agents_path.exists():
|
|
487
|
+
existing = agents_path.read_text(encoding="utf-8")
|
|
488
|
+
|
|
489
|
+
# Idempotency check
|
|
490
|
+
if SLM_MARKER_START in existing:
|
|
491
|
+
return "unchanged"
|
|
492
|
+
|
|
493
|
+
# Append SLM section
|
|
494
|
+
section = (
|
|
495
|
+
f"\n{SLM_MARKER_START}\n"
|
|
496
|
+
f"{source_content.strip()}\n"
|
|
497
|
+
f"{SLM_MARKER_END}\n"
|
|
498
|
+
)
|
|
499
|
+
agents_path.parent.mkdir(parents=True, exist_ok=True)
|
|
500
|
+
# Atomic write: agents_path is the user's hand-written rules file
|
|
501
|
+
# (.cursorrules, AGENTS.md, copilot-instructions.md, ...). A crash mid-write
|
|
502
|
+
# must NOT truncate it. tmp in the same dir → os.replace is atomic.
|
|
503
|
+
_tmp = agents_path.with_suffix(agents_path.suffix + ".tmp")
|
|
504
|
+
_tmp.write_text(existing + section, encoding="utf-8")
|
|
505
|
+
os.replace(_tmp, agents_path)
|
|
506
|
+
return "wrote"
|
|
@@ -28,10 +28,14 @@ lost (reaper finalizes everything at neutral 0.5).
|
|
|
28
28
|
MCP uses this as the default when the tool caller omits
|
|
29
29
|
``session_id``.
|
|
30
30
|
|
|
31
|
-
Concurrency:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
Concurrency: each write is atomic via write-temp + ``os.replace`` (atomic on
|
|
32
|
+
POSIX/Windows), so a concurrent reader never sees a half-written file —
|
|
33
|
+
last-writer-wins. This is best-effort, not lock-serialised: a concurrent
|
|
34
|
+
read-modify-write may lose an interleaved update, which is acceptable because
|
|
35
|
+
the registry only drives session attribution for closed-loop learning, not
|
|
36
|
+
memory correctness. Rollover: entries older than 1 hour are pruned on every
|
|
37
|
+
write. Fail-soft: every error path returns empty or the passed default — the
|
|
38
|
+
learning loop must never crash the hot path.
|
|
35
39
|
|
|
36
40
|
This is not a perfect correlation channel; two Claude sessions
|
|
37
41
|
typing in the same second can race. For single-user workstations
|