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,602 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# WP-09: MCP→CLI fallback adapter (ships INERT — DQ-2=B)
|
|
4
|
+
|
|
5
|
+
"""MCP→CLI fallback adapter for SuperLocalMemory.
|
|
6
|
+
|
|
7
|
+
When an MCP tool call fails, this module re-executes the equivalent
|
|
8
|
+
``slm <verb> --json`` command as a subprocess and reshapes the result
|
|
9
|
+
into the tool's expected MCP envelope shape.
|
|
10
|
+
|
|
11
|
+
Key design decisions:
|
|
12
|
+
- NEVER raises: every code path returns dict|None.
|
|
13
|
+
- shell=False always: argv is always a list.
|
|
14
|
+
- Only verified CLI verbs used: see TOOL_CLI_MAP (all argv[0] values are
|
|
15
|
+
confirmed subparsers in cli/main.py).
|
|
16
|
+
- Non-backed tools (no CLI equivalent) return {success:False,
|
|
17
|
+
fallback:"unavailable"} immediately without spawning a subprocess.
|
|
18
|
+
- Ships INERT (DQ-2=B): nothing calls this module yet. Wiring to
|
|
19
|
+
tools_core.py except-branches is deferred to a future audited diff.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
import shutil
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any, Callable
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Data structures
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class CliVerb:
|
|
43
|
+
"""Describes how to call ``slm <verb>`` for a specific MCP tool.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
argv: The base argv after the ``slm`` binary, e.g. ``["recall"]``
|
|
47
|
+
or ``["list", "-n"]``. NEVER includes ``--json`` (added at
|
|
48
|
+
call time) or the ``slm`` binary itself.
|
|
49
|
+
build_args: Callable that takes the MCP tool's ``args`` dict and
|
|
50
|
+
returns extra argv tokens (positional values, flags).
|
|
51
|
+
Must return a list; never raises.
|
|
52
|
+
reshape: Callable that takes the ``data`` field from a successful
|
|
53
|
+
CLI JSON envelope and returns a dict in the MCP tool's
|
|
54
|
+
return shape. ``via`` and ``degraded`` are added by the
|
|
55
|
+
caller — do not include them here.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
argv: list[str]
|
|
59
|
+
build_args: Callable[[dict[str, Any]], list[str]]
|
|
60
|
+
reshape: Callable[[dict[str, Any]], dict[str, Any]]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Helper builders (keep anonymous lambdas minimal; use named functions for
|
|
65
|
+
# anything non-trivial to aid testing and readability)
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _no_args(_: dict[str, Any]) -> list[str]:
|
|
70
|
+
"""No extra args needed."""
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _build_recall(args: dict[str, Any]) -> list[str]:
|
|
75
|
+
extra: list[str] = []
|
|
76
|
+
query = args.get("query", "")
|
|
77
|
+
if query:
|
|
78
|
+
extra.append(str(query))
|
|
79
|
+
limit = args.get("limit")
|
|
80
|
+
if limit is not None:
|
|
81
|
+
extra += ["--limit", str(int(limit))]
|
|
82
|
+
return extra
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _reshape_recall(data: dict[str, Any]) -> dict[str, Any]:
|
|
86
|
+
return {
|
|
87
|
+
"results": data.get("results", []),
|
|
88
|
+
"count": data.get("count", 0),
|
|
89
|
+
"query": data.get("query", ""),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _build_remember(args: dict[str, Any]) -> list[str]:
|
|
94
|
+
content = args.get("content", "")
|
|
95
|
+
extra: list[str] = [str(content)] if content else []
|
|
96
|
+
tags = args.get("tags", "")
|
|
97
|
+
if tags:
|
|
98
|
+
extra += ["--tags", str(tags)]
|
|
99
|
+
return extra
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _reshape_remember(data: dict[str, Any]) -> dict[str, Any]:
|
|
103
|
+
# Handle both sync {fact_ids, count} and async {queued, pending_id}
|
|
104
|
+
if "fact_ids" in data or "count" in data:
|
|
105
|
+
return {
|
|
106
|
+
"fact_ids": data.get("fact_ids", []),
|
|
107
|
+
"count": data.get("count", 0),
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
"queued": data.get("queued", False),
|
|
111
|
+
"pending_id": data.get("pending_id", ""),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _build_list_recent(args: dict[str, Any]) -> list[str]:
|
|
116
|
+
limit = args.get("limit", 20)
|
|
117
|
+
return ["-n", str(int(limit))]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _build_delete_memory(args: dict[str, Any]) -> list[str]:
|
|
121
|
+
fact_id = args.get("fact_id", "")
|
|
122
|
+
return [str(fact_id), "--yes"] if fact_id else ["--yes"]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_update_memory(args: dict[str, Any]) -> list[str]:
|
|
126
|
+
fact_id = args.get("fact_id", "")
|
|
127
|
+
content = args.get("content", "")
|
|
128
|
+
extra: list[str] = []
|
|
129
|
+
if fact_id:
|
|
130
|
+
extra.append(str(fact_id))
|
|
131
|
+
if content:
|
|
132
|
+
extra.append(str(content))
|
|
133
|
+
return extra
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _build_forget(args: dict[str, Any]) -> list[str]:
|
|
137
|
+
query = args.get("query", "")
|
|
138
|
+
return [str(query), "--yes"] if query else ["--yes"]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _build_consolidate_cognitive(_: dict[str, Any]) -> list[str]:
|
|
142
|
+
return ["--cognitive"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _build_set_mode(args: dict[str, Any]) -> list[str]:
|
|
146
|
+
mode = args.get("mode", "")
|
|
147
|
+
return [str(mode)] if mode else []
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _build_profile_switch(args: dict[str, Any]) -> list[str]:
|
|
151
|
+
profile_id = args.get("profile_id", "")
|
|
152
|
+
extra: list[str] = ["switch"]
|
|
153
|
+
if profile_id:
|
|
154
|
+
extra.append(str(profile_id))
|
|
155
|
+
return extra
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _build_trace(args: dict[str, Any]) -> list[str]:
|
|
159
|
+
query = args.get("query", "")
|
|
160
|
+
extra = [str(query)] if query else []
|
|
161
|
+
limit = args.get("limit")
|
|
162
|
+
if limit is not None:
|
|
163
|
+
extra += ["--limit", str(int(limit))]
|
|
164
|
+
return extra
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _passthrough(data: dict[str, Any]) -> dict[str, Any]:
|
|
168
|
+
"""Return data unchanged — reshaping not required for this tool."""
|
|
169
|
+
return dict(data)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# TOOL_CLI_MAP — ONLY verbs verified present in cli/main.py
|
|
174
|
+
# Verified via: grep "sub\.add_parser" cli/main.py
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
TOOL_CLI_MAP: dict[str, CliVerb] = {
|
|
178
|
+
# recall + search alias (recall has aliases=["search"] in argparse)
|
|
179
|
+
"recall": CliVerb(
|
|
180
|
+
argv=["recall"],
|
|
181
|
+
build_args=_build_recall,
|
|
182
|
+
reshape=_reshape_recall,
|
|
183
|
+
),
|
|
184
|
+
"search": CliVerb(
|
|
185
|
+
argv=["search"],
|
|
186
|
+
build_args=_build_recall,
|
|
187
|
+
reshape=_reshape_recall,
|
|
188
|
+
),
|
|
189
|
+
# remember — handles both sync {fact_ids,count} and async {queued,pending_id}
|
|
190
|
+
"remember": CliVerb(
|
|
191
|
+
argv=["remember"],
|
|
192
|
+
build_args=_build_remember,
|
|
193
|
+
reshape=_reshape_remember,
|
|
194
|
+
),
|
|
195
|
+
# list_recent → slm list -n <limit>
|
|
196
|
+
"list_recent": CliVerb(
|
|
197
|
+
argv=["list"],
|
|
198
|
+
build_args=_build_list_recent,
|
|
199
|
+
reshape=_passthrough,
|
|
200
|
+
),
|
|
201
|
+
# delete_memory → slm delete <fact_id> --yes
|
|
202
|
+
"delete_memory": CliVerb(
|
|
203
|
+
argv=["delete"],
|
|
204
|
+
build_args=_build_delete_memory,
|
|
205
|
+
reshape=_passthrough,
|
|
206
|
+
),
|
|
207
|
+
# update_memory → slm update <fact_id> <content>
|
|
208
|
+
"update_memory": CliVerb(
|
|
209
|
+
argv=["update"],
|
|
210
|
+
build_args=_build_update_memory,
|
|
211
|
+
reshape=_passthrough,
|
|
212
|
+
),
|
|
213
|
+
# get_status → slm status
|
|
214
|
+
"get_status": CliVerb(
|
|
215
|
+
argv=["status"],
|
|
216
|
+
build_args=_no_args,
|
|
217
|
+
reshape=_passthrough,
|
|
218
|
+
),
|
|
219
|
+
# forget → slm forget <query> --yes
|
|
220
|
+
"forget": CliVerb(
|
|
221
|
+
argv=["forget"],
|
|
222
|
+
build_args=_build_forget,
|
|
223
|
+
reshape=_passthrough,
|
|
224
|
+
),
|
|
225
|
+
# consolidate_cognitive → slm consolidate --cognitive
|
|
226
|
+
"consolidate_cognitive": CliVerb(
|
|
227
|
+
argv=["consolidate"],
|
|
228
|
+
build_args=_build_consolidate_cognitive,
|
|
229
|
+
reshape=_passthrough,
|
|
230
|
+
),
|
|
231
|
+
# get_soft_prompts → slm soft-prompts
|
|
232
|
+
"get_soft_prompts": CliVerb(
|
|
233
|
+
argv=["soft-prompts"],
|
|
234
|
+
build_args=_no_args,
|
|
235
|
+
reshape=_passthrough,
|
|
236
|
+
),
|
|
237
|
+
# set_mode → slm mode <m>
|
|
238
|
+
"set_mode": CliVerb(
|
|
239
|
+
argv=["mode"],
|
|
240
|
+
build_args=_build_set_mode,
|
|
241
|
+
reshape=_passthrough,
|
|
242
|
+
),
|
|
243
|
+
# get_mode → slm mode (no argument = get current)
|
|
244
|
+
"get_mode": CliVerb(
|
|
245
|
+
argv=["mode"],
|
|
246
|
+
build_args=_no_args,
|
|
247
|
+
reshape=_passthrough,
|
|
248
|
+
),
|
|
249
|
+
# recall_trace → slm trace <query>
|
|
250
|
+
"recall_trace": CliVerb(
|
|
251
|
+
argv=["trace"],
|
|
252
|
+
build_args=_build_trace,
|
|
253
|
+
reshape=_passthrough,
|
|
254
|
+
),
|
|
255
|
+
# quantize → slm quantize
|
|
256
|
+
"quantize": CliVerb(
|
|
257
|
+
argv=["quantize"],
|
|
258
|
+
build_args=_no_args,
|
|
259
|
+
reshape=_passthrough,
|
|
260
|
+
),
|
|
261
|
+
# health → slm health
|
|
262
|
+
"health": CliVerb(
|
|
263
|
+
argv=["health"],
|
|
264
|
+
build_args=_no_args,
|
|
265
|
+
reshape=_passthrough,
|
|
266
|
+
),
|
|
267
|
+
# slm_optimize_stats → slm optimize status (degraded)
|
|
268
|
+
"slm_optimize_stats": CliVerb(
|
|
269
|
+
argv=["optimize", "status"],
|
|
270
|
+
build_args=_no_args,
|
|
271
|
+
reshape=_passthrough,
|
|
272
|
+
),
|
|
273
|
+
# slm_cache_get → slm cache status (degraded — no key-level get in CLI)
|
|
274
|
+
"slm_cache_get": CliVerb(
|
|
275
|
+
argv=["cache", "status"],
|
|
276
|
+
build_args=_no_args,
|
|
277
|
+
reshape=_passthrough,
|
|
278
|
+
),
|
|
279
|
+
# compact_memories → slm compress status (degraded)
|
|
280
|
+
"compact_memories": CliVerb(
|
|
281
|
+
argv=["compress", "status"],
|
|
282
|
+
build_args=_no_args,
|
|
283
|
+
reshape=_passthrough,
|
|
284
|
+
),
|
|
285
|
+
# switch_profile → slm profile switch <profile_id>
|
|
286
|
+
"switch_profile": CliVerb(
|
|
287
|
+
argv=["profile"],
|
|
288
|
+
build_args=_build_profile_switch,
|
|
289
|
+
reshape=_passthrough,
|
|
290
|
+
),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
# NON_BACKED_TOOLS — tools with no viable CLI fallback
|
|
296
|
+
# Reasons:
|
|
297
|
+
# - fetch: no single-fact CLI fetch verb
|
|
298
|
+
# - code-graph (20): CLI verbs only exist under slm compress (toggles), not query
|
|
299
|
+
# - slm_retrieve/slm_cache_set: cache set/key retrieval not exposed in CLI
|
|
300
|
+
# - observe: lacks --json output (argparse exit or no structured output)
|
|
301
|
+
# - evolve_skill/skill_health/skill_lineage: learning tools, no CLI equivalent
|
|
302
|
+
# - get_version: --version is plain text, not --json envelope
|
|
303
|
+
# - all 8 mesh: P2P daemon-backed, no CLI equivalent
|
|
304
|
+
# - learning/assertion: no CLI equivalent
|
|
305
|
+
# - session_init/close_session/run_maintenance: daemon-implicit lifecycle
|
|
306
|
+
# - report_feedback/report_outcome: no CLI equivalent
|
|
307
|
+
# - get_lifecycle_status/set_retention_policy: no CLI equivalent
|
|
308
|
+
# - get_behavioral_patterns/audit_trail: no CLI equivalent
|
|
309
|
+
# - consistency_check/get_retention_stats: no CLI equivalent
|
|
310
|
+
# - backup_status/memory_used/get_learned_patterns/correct_pattern: no CLI equivalent
|
|
311
|
+
# - get_attribution/build_graph/core_memory/reap_processes: no CLI equivalent
|
|
312
|
+
# - slm_compress: no CLI read equivalent (compress is write-only control)
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
NON_BACKED_TOOLS: frozenset[str] = frozenset({
|
|
316
|
+
# Fetch — no single-fact CLI verb
|
|
317
|
+
"fetch",
|
|
318
|
+
# Code-graph tools (20)
|
|
319
|
+
"build_code_graph",
|
|
320
|
+
"update_code_graph",
|
|
321
|
+
"get_blast_radius",
|
|
322
|
+
"get_review_context",
|
|
323
|
+
"query_graph",
|
|
324
|
+
"semantic_search_code",
|
|
325
|
+
"list_graph_stats",
|
|
326
|
+
"find_large_functions",
|
|
327
|
+
"list_flows",
|
|
328
|
+
"get_flow",
|
|
329
|
+
"get_affected_flows",
|
|
330
|
+
"list_communities",
|
|
331
|
+
"get_community",
|
|
332
|
+
"get_architecture_overview",
|
|
333
|
+
"detect_changes",
|
|
334
|
+
"refactor_preview",
|
|
335
|
+
"apply_refactor",
|
|
336
|
+
"code_memory_search",
|
|
337
|
+
"code_entity_history",
|
|
338
|
+
"enrich_blast_radius",
|
|
339
|
+
# Optimize tools without usable CLI read path
|
|
340
|
+
"slm_retrieve",
|
|
341
|
+
"slm_cache_set",
|
|
342
|
+
"slm_compress",
|
|
343
|
+
# observe/evolve lack --json or reliable structured output
|
|
344
|
+
"observe",
|
|
345
|
+
"evolve_skill",
|
|
346
|
+
# Version is plain text
|
|
347
|
+
"get_version",
|
|
348
|
+
# Mesh P2P tools (8)
|
|
349
|
+
"mesh_summary",
|
|
350
|
+
"mesh_peers",
|
|
351
|
+
"mesh_send",
|
|
352
|
+
"mesh_inbox",
|
|
353
|
+
"mesh_state",
|
|
354
|
+
"mesh_lock",
|
|
355
|
+
"mesh_events",
|
|
356
|
+
"mesh_status",
|
|
357
|
+
# Learning / assertion tools
|
|
358
|
+
"log_tool_event",
|
|
359
|
+
"get_assertions",
|
|
360
|
+
"reinforce_assertion",
|
|
361
|
+
"contradict_assertion",
|
|
362
|
+
# Evolution tools
|
|
363
|
+
"skill_health",
|
|
364
|
+
"skill_lineage",
|
|
365
|
+
# Session lifecycle (daemon-implicit)
|
|
366
|
+
"session_init",
|
|
367
|
+
"close_session",
|
|
368
|
+
"run_maintenance",
|
|
369
|
+
# Feedback / reporting
|
|
370
|
+
"report_feedback",
|
|
371
|
+
"report_outcome",
|
|
372
|
+
# V28 tools — no CLI equivalent
|
|
373
|
+
"get_lifecycle_status",
|
|
374
|
+
"set_retention_policy",
|
|
375
|
+
"get_behavioral_patterns",
|
|
376
|
+
"audit_trail",
|
|
377
|
+
# V3 tools without clean CLI fallback
|
|
378
|
+
"consistency_check",
|
|
379
|
+
"get_retention_stats",
|
|
380
|
+
# Core extras without CLI read equivalent
|
|
381
|
+
"backup_status",
|
|
382
|
+
"memory_used",
|
|
383
|
+
"get_learned_patterns",
|
|
384
|
+
"correct_pattern",
|
|
385
|
+
"get_attribution",
|
|
386
|
+
"build_graph",
|
|
387
|
+
"core_memory",
|
|
388
|
+
"reap_processes",
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
# Internal helpers
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _resolve_slm(slm_path: str | None = None) -> str | None:
|
|
398
|
+
"""Resolve the ``slm`` CLI binary path.
|
|
399
|
+
|
|
400
|
+
Probe order:
|
|
401
|
+
1. Explicit ``slm_path`` argument (from caller).
|
|
402
|
+
2. ``SLM_CLI_PATH`` environment variable.
|
|
403
|
+
3. ``shutil.which("slm")`` — PATH lookup.
|
|
404
|
+
4. venv-adjacent ``bin/slm`` (same Python prefix).
|
|
405
|
+
|
|
406
|
+
Returns the first valid path string found, or None if not found.
|
|
407
|
+
Never raises.
|
|
408
|
+
"""
|
|
409
|
+
# 1. Explicit override from caller
|
|
410
|
+
if slm_path:
|
|
411
|
+
return slm_path
|
|
412
|
+
|
|
413
|
+
# 2. Environment variable
|
|
414
|
+
env_path = os.environ.get("SLM_CLI_PATH", "").strip()
|
|
415
|
+
if env_path:
|
|
416
|
+
return env_path
|
|
417
|
+
|
|
418
|
+
# 3. PATH lookup
|
|
419
|
+
which_result = shutil.which("slm")
|
|
420
|
+
if which_result:
|
|
421
|
+
return which_result
|
|
422
|
+
|
|
423
|
+
# 4. Venv-adjacent: look for slm next to the current Python interpreter
|
|
424
|
+
python_bin = Path(sys.executable)
|
|
425
|
+
candidate = python_bin.parent / "slm"
|
|
426
|
+
if candidate.exists():
|
|
427
|
+
return str(candidate)
|
|
428
|
+
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _parse_json_from_stdout(stdout: str) -> dict[str, Any] | None:
|
|
433
|
+
"""Parse JSON from subprocess stdout, handling banner text prefix.
|
|
434
|
+
|
|
435
|
+
The SLM CLI emits JSON on stdout (banner goes to stderr). However,
|
|
436
|
+
some environments or older versions may emit banner text before the
|
|
437
|
+
JSON on stdout. We use brace-scanning to find the first ``{`` and
|
|
438
|
+
attempt to parse from there.
|
|
439
|
+
|
|
440
|
+
Returns dict if parsed successfully, None otherwise. Never raises.
|
|
441
|
+
"""
|
|
442
|
+
if not stdout:
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
# Fast path: entire stdout is valid JSON
|
|
446
|
+
try:
|
|
447
|
+
parsed = json.loads(stdout)
|
|
448
|
+
if isinstance(parsed, dict):
|
|
449
|
+
return parsed
|
|
450
|
+
except (json.JSONDecodeError, ValueError):
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
# Brace-scan fallback: find first '{' and try parsing from there
|
|
454
|
+
brace_idx = stdout.find("{")
|
|
455
|
+
if brace_idx == -1:
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
parsed = json.loads(stdout[brace_idx:])
|
|
460
|
+
if isinstance(parsed, dict):
|
|
461
|
+
return parsed
|
|
462
|
+
except (json.JSONDecodeError, ValueError):
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _run_slm(
|
|
469
|
+
argv: list[str],
|
|
470
|
+
timeout: float,
|
|
471
|
+
) -> dict[str, Any] | None:
|
|
472
|
+
"""Run ``slm`` as a subprocess and parse the JSON envelope from stdout.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
argv: Full command including the slm binary path, e.g.
|
|
476
|
+
["/usr/local/bin/slm", "recall", "hello", "--json"].
|
|
477
|
+
timeout: Subprocess timeout in seconds.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Parsed dict from stdout JSON, or None on timeout / OSError /
|
|
481
|
+
non-JSON output. Never raises.
|
|
482
|
+
"""
|
|
483
|
+
try:
|
|
484
|
+
result = subprocess.run(
|
|
485
|
+
argv,
|
|
486
|
+
capture_output=True,
|
|
487
|
+
text=True,
|
|
488
|
+
timeout=timeout,
|
|
489
|
+
shell=False,
|
|
490
|
+
check=False,
|
|
491
|
+
)
|
|
492
|
+
except subprocess.TimeoutExpired:
|
|
493
|
+
logger.warning("slm CLI timed out after %.1fs (argv=%r)", timeout, argv)
|
|
494
|
+
return None
|
|
495
|
+
except OSError as exc:
|
|
496
|
+
logger.warning("slm CLI OSError: %s (argv=%r)", exc, argv)
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
return _parse_json_from_stdout(result.stdout)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# ---------------------------------------------------------------------------
|
|
503
|
+
# Internal routing helpers (keep fallback_via_cli under 50 lines)
|
|
504
|
+
# ---------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _unavailable(reason: str, tool_name: str) -> dict[str, Any]:
|
|
508
|
+
"""Return a clean unavailable envelope."""
|
|
509
|
+
return {
|
|
510
|
+
"success": False,
|
|
511
|
+
"fallback": "unavailable",
|
|
512
|
+
"reason": reason,
|
|
513
|
+
"tool": tool_name,
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _invoke_backed(
|
|
518
|
+
verb: CliVerb,
|
|
519
|
+
safe_args: dict[str, Any],
|
|
520
|
+
tool_name: str,
|
|
521
|
+
slm_bin: str,
|
|
522
|
+
timeout: float,
|
|
523
|
+
) -> dict[str, Any] | None:
|
|
524
|
+
"""Run the CLI subprocess for a backed tool and return the shaped result.
|
|
525
|
+
|
|
526
|
+
Returns shaped dict on success, None if subprocess failed, or an
|
|
527
|
+
error dict if CLI returned success=False.
|
|
528
|
+
"""
|
|
529
|
+
try:
|
|
530
|
+
extra = verb.build_args(safe_args)
|
|
531
|
+
except Exception: # noqa: BLE001
|
|
532
|
+
extra = []
|
|
533
|
+
|
|
534
|
+
full_argv: list[str] = [slm_bin, *verb.argv, *extra, "--json"]
|
|
535
|
+
envelope = _run_slm(full_argv, timeout)
|
|
536
|
+
if envelope is None:
|
|
537
|
+
logger.warning("fallback_via_cli: subprocess returned no valid JSON for %r", tool_name)
|
|
538
|
+
return None
|
|
539
|
+
|
|
540
|
+
if not envelope.get("success", False):
|
|
541
|
+
return {
|
|
542
|
+
"success": False,
|
|
543
|
+
"error": envelope.get("error", {}),
|
|
544
|
+
"via": "cli_fallback",
|
|
545
|
+
"tool": tool_name,
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
data = envelope.get("data", {})
|
|
549
|
+
if not isinstance(data, dict):
|
|
550
|
+
data = {}
|
|
551
|
+
try:
|
|
552
|
+
shaped = verb.reshape(data)
|
|
553
|
+
except Exception: # noqa: BLE001
|
|
554
|
+
shaped = dict(data)
|
|
555
|
+
|
|
556
|
+
shaped["via"] = "cli_fallback"
|
|
557
|
+
shaped["degraded"] = True
|
|
558
|
+
return shaped
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ---------------------------------------------------------------------------
|
|
562
|
+
# Public API
|
|
563
|
+
# ---------------------------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def fallback_via_cli(
|
|
567
|
+
tool_name: Any,
|
|
568
|
+
args: Any,
|
|
569
|
+
*,
|
|
570
|
+
timeout: float = 20.0,
|
|
571
|
+
slm_path: str | None = None,
|
|
572
|
+
) -> dict[str, Any] | None:
|
|
573
|
+
"""Attempt to execute an MCP tool via the ``slm`` CLI as a fallback.
|
|
574
|
+
|
|
575
|
+
Called from an MCP tool's except branch when the normal code path fails.
|
|
576
|
+
NEVER raises — all exceptions are caught internally.
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
- dict (tool MCP shape + ``via:"cli_fallback"`` + ``degraded:True``)
|
|
580
|
+
on success.
|
|
581
|
+
- ``{success:False, fallback:"unavailable"}`` for non-backed / unmapped.
|
|
582
|
+
- ``None`` when backed but subprocess failed (caller surfaces MCP error).
|
|
583
|
+
"""
|
|
584
|
+
if not isinstance(tool_name, str):
|
|
585
|
+
return _unavailable("invalid_tool_name", "")
|
|
586
|
+
safe_args: dict[str, Any] = args if isinstance(args, dict) else {}
|
|
587
|
+
|
|
588
|
+
if tool_name in NON_BACKED_TOOLS:
|
|
589
|
+
logger.debug("fallback_via_cli: %r is NON_BACKED", tool_name)
|
|
590
|
+
return _unavailable("no_cli_verb", tool_name)
|
|
591
|
+
|
|
592
|
+
verb = TOOL_CLI_MAP.get(tool_name)
|
|
593
|
+
if verb is None:
|
|
594
|
+
logger.debug("fallback_via_cli: %r not in TOOL_CLI_MAP", tool_name)
|
|
595
|
+
return _unavailable("unmapped", tool_name)
|
|
596
|
+
|
|
597
|
+
slm_bin = _resolve_slm(slm_path)
|
|
598
|
+
if slm_bin is None:
|
|
599
|
+
logger.warning("fallback_via_cli: slm binary not found — cannot fallback %r", tool_name)
|
|
600
|
+
return None
|
|
601
|
+
|
|
602
|
+
return _invoke_backed(verb, safe_args, tool_name, slm_bin, timeout)
|
|
@@ -135,6 +135,73 @@ _all_tools = _os_reg.environ.get("SLM_MCP_ALL_TOOLS") == "1"
|
|
|
135
135
|
# exactly the tools they invoke. Falls back to _ESSENTIAL_TOOLS when unset.
|
|
136
136
|
_user_allowlist_str = _os_reg.environ.get("SLM_MCP_TOOLS", "").strip()
|
|
137
137
|
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# v3.6.14 WP-01: Named profile definitions
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
_PROFILE_CORE = frozenset({ # 14
|
|
143
|
+
"remember", "recall", "search", "fetch", "list_recent", "update_memory", "forget",
|
|
144
|
+
"session_init", "close_session",
|
|
145
|
+
"slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
|
|
146
|
+
})
|
|
147
|
+
_PROFILE_CODE = _PROFILE_CORE | frozenset({ # 20
|
|
148
|
+
"build_code_graph", "get_blast_radius", "query_graph",
|
|
149
|
+
"semantic_search_code", "get_review_context", "detect_changes",
|
|
150
|
+
})
|
|
151
|
+
_PROFILE_FULL_MESH = frozenset({ # 8
|
|
152
|
+
"mesh_summary", "mesh_peers", "mesh_send", "mesh_inbox",
|
|
153
|
+
"mesh_state", "mesh_lock", "mesh_events", "mesh_status",
|
|
154
|
+
})
|
|
155
|
+
_PROFILE_FULL = frozenset({ # 30 base — EXPLICIT literal, NOT runtime _ESSENTIAL_TOOLS (OQ-2)
|
|
156
|
+
"remember", "recall", "search", "fetch", "list_recent", "delete_memory", "update_memory",
|
|
157
|
+
"get_status", "session_init", "observe", "close_session", "report_feedback", "forget",
|
|
158
|
+
"run_maintenance", "consolidate_cognitive", "get_soft_prompts", "set_mode", "report_outcome",
|
|
159
|
+
"log_tool_event", "get_assertions", "reinforce_assertion", "contradict_assertion",
|
|
160
|
+
"evolve_skill", "skill_health", "skill_lineage",
|
|
161
|
+
"slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
|
|
162
|
+
}) | _PROFILE_FULL_MESH # 38
|
|
163
|
+
_PROFILE_POWER = _PROFILE_FULL | frozenset({ # 50
|
|
164
|
+
"get_version", "get_mode", "health", "consistency_check", "recall_trace",
|
|
165
|
+
"get_lifecycle_status", "set_retention_policy", "compact_memories",
|
|
166
|
+
"get_behavioral_patterns", "audit_trail", "quantize", "get_retention_stats",
|
|
167
|
+
})
|
|
168
|
+
_PROFILE_MESH = _PROFILE_FULL_MESH # 8
|
|
169
|
+
|
|
170
|
+
_PROFILE_DEFINITIONS: dict[str, frozenset[str]] = {
|
|
171
|
+
"core": _PROFILE_CORE,
|
|
172
|
+
"code": _PROFILE_CODE,
|
|
173
|
+
"full": _PROFILE_FULL,
|
|
174
|
+
"power": _PROFILE_POWER,
|
|
175
|
+
"mesh": _PROFILE_MESH,
|
|
176
|
+
} # "whole" intentionally absent — maps to raw server (D-2 LOCKED)
|
|
177
|
+
|
|
178
|
+
_profile = _os_reg.environ.get("SLM_MCP_PROFILE", "").strip().lower()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _resolve_profile_allowed(
|
|
182
|
+
profile: str,
|
|
183
|
+
definitions: dict[str, frozenset[str]],
|
|
184
|
+
essential: frozenset[str],
|
|
185
|
+
) -> frozenset[str] | None:
|
|
186
|
+
"""Resolve a profile name to its allowed tool frozenset, or None for raw-server routes.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
None — for "" (no selection) or "whole" (raw server, all tools).
|
|
190
|
+
frozenset — the profile's tool set for known profiles.
|
|
191
|
+
essential — fail-open fallback for unknown non-empty profiles (+ warning).
|
|
192
|
+
"""
|
|
193
|
+
if not profile or profile == "whole":
|
|
194
|
+
return None
|
|
195
|
+
if profile in definitions:
|
|
196
|
+
return definitions[profile]
|
|
197
|
+
logger.warning(
|
|
198
|
+
"SLM_MCP_PROFILE=%r is not a recognised profile "
|
|
199
|
+
"(valid: %s, whole); falling back to essential tools.",
|
|
200
|
+
profile,
|
|
201
|
+
", ".join(sorted(definitions)),
|
|
202
|
+
)
|
|
203
|
+
return essential
|
|
204
|
+
|
|
138
205
|
|
|
139
206
|
class _FilteredServer:
|
|
140
207
|
"""Wraps FastMCP to only register essential tools.
|
|
@@ -161,14 +228,18 @@ class _FilteredServer:
|
|
|
161
228
|
return getattr(self._server, name)
|
|
162
229
|
|
|
163
230
|
|
|
164
|
-
# Choose registration target (precedence: ALL > user allowlist > essential)
|
|
231
|
+
# Choose registration target (precedence: ALL > user allowlist > profile > essential)
|
|
165
232
|
if _all_tools:
|
|
166
|
-
_target = server
|
|
233
|
+
_target = server # tier1 (unchanged)
|
|
167
234
|
elif _user_allowlist_str:
|
|
168
235
|
_user_allowlist = frozenset(t.strip() for t in _user_allowlist_str.split(",") if t.strip())
|
|
169
|
-
_target = _FilteredServer(server, _user_allowlist)
|
|
236
|
+
_target = _FilteredServer(server, _user_allowlist) # tier2 (unchanged)
|
|
237
|
+
elif _profile == "whole":
|
|
238
|
+
_target = server # NEW raw-server
|
|
239
|
+
elif _profile:
|
|
240
|
+
_target = _FilteredServer(server, _resolve_profile_allowed(_profile, _PROFILE_DEFINITIONS, _ESSENTIAL_TOOLS)) # NEW
|
|
170
241
|
else:
|
|
171
|
-
_target = _FilteredServer(server, _ESSENTIAL_TOOLS)
|
|
242
|
+
_target = _FilteredServer(server, _ESSENTIAL_TOOLS) # default (unchanged)
|
|
172
243
|
|
|
173
244
|
from superlocalmemory.mcp.tools_core import register_core_tools
|
|
174
245
|
from superlocalmemory.mcp.tools_v28 import register_v28_tools
|