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
|
@@ -94,6 +94,48 @@ def _cmd_help_optimize(args: Namespace) -> None:
|
|
|
94
94
|
# ---- end SLM v3.6 Optimize dispatch functions ----
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def cmd_session(args: Namespace) -> None:
|
|
98
|
+
"""#49: Open/close a session locally via the daemon — no model roundtrip,
|
|
99
|
+
so a shell hook (e.g. Claude session-start / /quit) can call it directly.
|
|
100
|
+
"""
|
|
101
|
+
from superlocalmemory.cli.daemon import (
|
|
102
|
+
daemon_request, ensure_daemon, is_daemon_running,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
action = getattr(args, "session_command", None)
|
|
106
|
+
if action not in ("open", "close"):
|
|
107
|
+
print("Usage: slm session {open|close} "
|
|
108
|
+
"[--session-id ID] [--project-path PATH] [--query Q]")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if not is_daemon_running():
|
|
112
|
+
ensure_daemon()
|
|
113
|
+
|
|
114
|
+
if action == "open":
|
|
115
|
+
body = {
|
|
116
|
+
"project_path": getattr(args, "project_path", "") or "",
|
|
117
|
+
"query": getattr(args, "query", "") or "",
|
|
118
|
+
"max_results": int(getattr(args, "max_results", 10) or 10),
|
|
119
|
+
}
|
|
120
|
+
resp = daemon_request("POST", "/session/open", body)
|
|
121
|
+
if resp and resp.get("ok"):
|
|
122
|
+
print(f"Session opened — warmed {resp.get('warmed', 0)} memories "
|
|
123
|
+
f"(query: {resp.get('query', '')})")
|
|
124
|
+
else:
|
|
125
|
+
print("Session open failed (daemon unreachable?)")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# close
|
|
129
|
+
body = {"session_id": getattr(args, "session_id", "") or ""}
|
|
130
|
+
resp = daemon_request("POST", "/session/close", body)
|
|
131
|
+
if resp and resp.get("ok"):
|
|
132
|
+
sid = resp.get("session_id") or "(most recent)"
|
|
133
|
+
print(f"Session closed: {sid} — "
|
|
134
|
+
f"{resp.get('summary_events_created', 0)} summary event(s) created")
|
|
135
|
+
else:
|
|
136
|
+
print("Session close failed (daemon unreachable?)")
|
|
137
|
+
|
|
138
|
+
|
|
97
139
|
def dispatch(args: Namespace) -> None:
|
|
98
140
|
"""Route CLI command to the appropriate handler."""
|
|
99
141
|
# Auto-install/upgrade hooks on version change (single file read, ~0.1ms)
|
|
@@ -128,6 +170,7 @@ def dispatch(args: Namespace) -> None:
|
|
|
128
170
|
"profile": cmd_profile,
|
|
129
171
|
"hooks": cmd_hooks,
|
|
130
172
|
"session-context": cmd_session_context,
|
|
173
|
+
"session": cmd_session, # #49: local session open/close for hooks
|
|
131
174
|
"observe": cmd_observe,
|
|
132
175
|
# V3.3 commands
|
|
133
176
|
"decay": cmd_decay,
|
|
@@ -780,8 +823,97 @@ def _cmd_context_dispatch(args: Namespace) -> None:
|
|
|
780
823
|
cmd_context(args)
|
|
781
824
|
|
|
782
825
|
|
|
826
|
+
def _agents_md_source_factory():
|
|
827
|
+
"""Return a callable that reads the WP-05 AGENTS.md content, or None on failure.
|
|
828
|
+
|
|
829
|
+
Source: plugin-src/rules/AGENTS.md (relative to package root).
|
|
830
|
+
Gracefully skips if absent — never fails the MCP write.
|
|
831
|
+
"""
|
|
832
|
+
from pathlib import Path
|
|
833
|
+
|
|
834
|
+
# Resolve relative to the package root (src/superlocalmemory/../../)
|
|
835
|
+
_pkg_root = Path(__file__).resolve().parents[3]
|
|
836
|
+
_agents_src = _pkg_root / "plugin-src" / "rules" / "AGENTS.md"
|
|
837
|
+
|
|
838
|
+
def _read() -> str | None:
|
|
839
|
+
if _agents_src.exists():
|
|
840
|
+
return _agents_src.read_text(encoding="utf-8")
|
|
841
|
+
logger.warning(
|
|
842
|
+
"WP-05 AGENTS.md not found at %s — skipping AGENTS.md write", _agents_src
|
|
843
|
+
)
|
|
844
|
+
return None
|
|
845
|
+
|
|
846
|
+
return _read
|
|
847
|
+
|
|
848
|
+
|
|
783
849
|
def cmd_connect(args: Namespace) -> None:
|
|
784
|
-
"""Configure IDE integrations.
|
|
850
|
+
"""Configure IDE integrations.
|
|
851
|
+
|
|
852
|
+
Dispatch priority (WP-08):
|
|
853
|
+
1. ``slm connect <ide>`` where ide ∈ IDE_MATRIX → portable_kit.connect_ide
|
|
854
|
+
(MCP-wiring + AGENTS.md; includes claude-code short-circuit to WP-06).
|
|
855
|
+
2. ``--cross-platform`` / ``--disable`` → LLD-05 CrossPlatformConnector.
|
|
856
|
+
3. Bare ``slm connect`` / ``--list`` → legacy IDEConnector (markdown-rules).
|
|
857
|
+
"""
|
|
858
|
+
ide_arg = getattr(args, "ide", None)
|
|
859
|
+
|
|
860
|
+
# WP-08: intercept known IDE_MATRIX ids before legacy branches (CRIT-1)
|
|
861
|
+
if ide_arg is not None:
|
|
862
|
+
from superlocalmemory.hooks.portable_kit import (
|
|
863
|
+
IDE_MATRIX,
|
|
864
|
+
connect_ide,
|
|
865
|
+
supported_ides,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
if ide_arg in IDE_MATRIX:
|
|
869
|
+
here = getattr(args, "here", False)
|
|
870
|
+
profile = getattr(args, "profile", None)
|
|
871
|
+
project = None
|
|
872
|
+
if here:
|
|
873
|
+
import pathlib
|
|
874
|
+
project = pathlib.Path.cwd()
|
|
875
|
+
|
|
876
|
+
result = connect_ide(
|
|
877
|
+
ide_arg,
|
|
878
|
+
home=None,
|
|
879
|
+
project=project,
|
|
880
|
+
here=here,
|
|
881
|
+
profile=profile,
|
|
882
|
+
agents_md_source=_agents_md_source_factory(),
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
if getattr(args, "json", False):
|
|
886
|
+
from superlocalmemory.cli.json_output import json_print
|
|
887
|
+
json_print("connect", data=result)
|
|
888
|
+
return
|
|
889
|
+
|
|
890
|
+
if result["error"]:
|
|
891
|
+
print(f"Error: {result['error']}", file=sys.stderr)
|
|
892
|
+
print(
|
|
893
|
+
f"Supported IDEs: {', '.join(supported_ides())}",
|
|
894
|
+
file=sys.stderr,
|
|
895
|
+
)
|
|
896
|
+
sys.exit(1)
|
|
897
|
+
|
|
898
|
+
status_sym = {"wrote": "[+]", "merged": "[~]", "unchanged": "[=]",
|
|
899
|
+
"skipped": "[s]", "error": "[!]"}.get(
|
|
900
|
+
result["mcp_config"], "[?]"
|
|
901
|
+
)
|
|
902
|
+
print(
|
|
903
|
+
f"{status_sym} {ide_arg}: mcp_config={result['mcp_config']} "
|
|
904
|
+
f"path={result['mcp_path']}"
|
|
905
|
+
)
|
|
906
|
+
print(f" agents_md={result['agents_md']}")
|
|
907
|
+
return
|
|
908
|
+
|
|
909
|
+
# Unknown ide — list supported and exit non-zero
|
|
910
|
+
from superlocalmemory.hooks.portable_kit import supported_ides
|
|
911
|
+
print(
|
|
912
|
+
f"Unknown IDE '{ide_arg}'.\nSupported: {', '.join(supported_ides())}",
|
|
913
|
+
file=sys.stderr,
|
|
914
|
+
)
|
|
915
|
+
sys.exit(1)
|
|
916
|
+
|
|
785
917
|
# Route --disable <name> and --cross-platform to the LLD-05 orchestrator.
|
|
786
918
|
if getattr(args, "disable", None) or getattr(args, "cross_platform", False):
|
|
787
919
|
from superlocalmemory.cli.context_commands import (
|
|
@@ -904,6 +1036,19 @@ def cmd_remember(args: Namespace) -> None:
|
|
|
904
1036
|
|
|
905
1037
|
use_json = getattr(args, 'json', False)
|
|
906
1038
|
sync_mode = getattr(args, 'sync_mode', False)
|
|
1039
|
+
# v3.6.15 multi-scope: scope=None means "not specified" → resolve to the
|
|
1040
|
+
# configured default_scope (personal) at the daemon / engine boundary.
|
|
1041
|
+
# Shared memory is opt-in, so an unset --scope always stays private.
|
|
1042
|
+
scope = getattr(args, 'scope', None)
|
|
1043
|
+
# v3.6.15: --shared-with is a comma-separated string on the CLI, but the
|
|
1044
|
+
# daemon (RememberRequest) and engine expect list[str]. Parse here so an
|
|
1045
|
+
# explicit `--shared-with a,b` doesn't 422 at the daemon and silently fall
|
|
1046
|
+
# back to a personal write.
|
|
1047
|
+
_sw_raw = getattr(args, 'shared_with', None)
|
|
1048
|
+
shared_with = (
|
|
1049
|
+
[s.strip() for s in _sw_raw.split(",") if s.strip()]
|
|
1050
|
+
if isinstance(_sw_raw, str) and _sw_raw.strip() else _sw_raw
|
|
1051
|
+
)
|
|
907
1052
|
|
|
908
1053
|
# V3.3.21: Route through daemon for instant remember (no cold start).
|
|
909
1054
|
# If daemon is running, send request directly (~0.1s).
|
|
@@ -916,6 +1061,8 @@ def cmd_remember(args: Namespace) -> None:
|
|
|
916
1061
|
result = daemon_request("POST", "/remember", {
|
|
917
1062
|
"content": args.content,
|
|
918
1063
|
"tags": args.tags or "",
|
|
1064
|
+
"scope": scope,
|
|
1065
|
+
"shared_with": shared_with,
|
|
919
1066
|
})
|
|
920
1067
|
if result and "fact_ids" in result:
|
|
921
1068
|
if use_json:
|
|
@@ -931,9 +1078,20 @@ def cmd_remember(args: Namespace) -> None:
|
|
|
931
1078
|
# NO subprocess spawn. Daemon's background loop picks up pending memories.
|
|
932
1079
|
from superlocalmemory.cli.pending_store import store_pending
|
|
933
1080
|
|
|
1081
|
+
# v3.6.15 multi-scope: carry an explicit non-personal scope into the
|
|
1082
|
+
# pending row's metadata so the materializer replays the right
|
|
1083
|
+
# visibility. Unset / personal carries nothing — byte-identical to
|
|
1084
|
+
# pre-3.6.15 pending rows.
|
|
1085
|
+
_pending_meta = None
|
|
1086
|
+
if scope and scope != "personal":
|
|
1087
|
+
_pending_meta = {"scope": scope}
|
|
1088
|
+
if shared_with:
|
|
1089
|
+
_pending_meta["shared_with"] = shared_with
|
|
1090
|
+
|
|
934
1091
|
row_id = store_pending(
|
|
935
1092
|
content=args.content,
|
|
936
1093
|
tags=args.tags or "",
|
|
1094
|
+
metadata=_pending_meta,
|
|
937
1095
|
)
|
|
938
1096
|
|
|
939
1097
|
if use_json:
|
|
@@ -951,8 +1109,13 @@ def cmd_remember(args: Namespace) -> None:
|
|
|
951
1109
|
engine = MemoryEngine(config)
|
|
952
1110
|
engine.initialize()
|
|
953
1111
|
|
|
1112
|
+
# v3.6.15: resolve an unset scope to the configured default_scope.
|
|
1113
|
+
_scope = scope or getattr(getattr(config, "scope", None), "default_scope", "personal")
|
|
954
1114
|
metadata = {"tags": args.tags} if args.tags else {}
|
|
955
|
-
fact_ids = engine.store(
|
|
1115
|
+
fact_ids = engine.store(
|
|
1116
|
+
args.content, metadata=metadata,
|
|
1117
|
+
scope=_scope, shared_with=shared_with,
|
|
1118
|
+
)
|
|
956
1119
|
except Exception as exc:
|
|
957
1120
|
if use_json:
|
|
958
1121
|
from superlocalmemory.cli.json_output import json_print
|
|
@@ -975,6 +1138,11 @@ def cmd_remember(args: Namespace) -> None:
|
|
|
975
1138
|
def cmd_recall(args: Namespace) -> None:
|
|
976
1139
|
"""Search memories via the engine — routes through daemon if available."""
|
|
977
1140
|
use_json = getattr(args, 'json', False)
|
|
1141
|
+
# v3.6.15: None = "not specified" → daemon/engine resolves the configured
|
|
1142
|
+
# default (shared-off). Only an explicit --include-global / --no-global
|
|
1143
|
+
# produces True/False here.
|
|
1144
|
+
include_global = getattr(args, 'include_global', None)
|
|
1145
|
+
include_shared = getattr(args, 'include_shared', None)
|
|
978
1146
|
|
|
979
1147
|
# V3.3.21: Route through daemon for instant response (no cold start).
|
|
980
1148
|
# Falls back to direct engine if daemon not running.
|
|
@@ -988,10 +1156,18 @@ def cmd_recall(args: Namespace) -> None:
|
|
|
988
1156
|
from urllib.parse import quote
|
|
989
1157
|
session_id = f"cli:{os.getppid()}"
|
|
990
1158
|
fast_qs = "&fast=true" if getattr(args, "fast", False) else ""
|
|
1159
|
+
# Only send scope flags when the user set them explicitly; absent =
|
|
1160
|
+
# let the daemon resolve the configured default. (Never emit
|
|
1161
|
+
# "none" — that would parse as a missing/false value.)
|
|
1162
|
+
scope_qs = ""
|
|
1163
|
+
if include_global is not None:
|
|
1164
|
+
scope_qs += f"&include_global={str(include_global).lower()}"
|
|
1165
|
+
if include_shared is not None:
|
|
1166
|
+
scope_qs += f"&include_shared={str(include_shared).lower()}"
|
|
991
1167
|
result = daemon_request(
|
|
992
1168
|
"GET",
|
|
993
1169
|
f"/recall?q={quote(args.query)}&limit={args.limit}"
|
|
994
|
-
f"&session_id={quote(session_id)}{fast_qs}",
|
|
1170
|
+
f"&session_id={quote(session_id)}{fast_qs}{scope_qs}",
|
|
995
1171
|
)
|
|
996
1172
|
if result and "results" in result:
|
|
997
1173
|
# Format daemon response same as engine response
|
|
@@ -1025,6 +1201,8 @@ def cmd_recall(args: Namespace) -> None:
|
|
|
1025
1201
|
response = engine.recall(
|
|
1026
1202
|
args.query, limit=args.limit,
|
|
1027
1203
|
fast=getattr(args, "fast", False),
|
|
1204
|
+
include_global=include_global,
|
|
1205
|
+
include_shared=include_shared,
|
|
1028
1206
|
)
|
|
1029
1207
|
except Exception as exc:
|
|
1030
1208
|
if use_json:
|
|
@@ -1318,14 +1496,60 @@ def cmd_status(args: Namespace) -> None:
|
|
|
1318
1496
|
|
|
1319
1497
|
if getattr(args, 'json', False):
|
|
1320
1498
|
from superlocalmemory.cli.json_output import json_print
|
|
1499
|
+
|
|
1500
|
+
# WP-02 D8: canonical key set — db_size_mb always present (0.0 if absent).
|
|
1501
|
+
db_size_mb = 0.0
|
|
1502
|
+
if config.db_path.exists():
|
|
1503
|
+
db_size_mb = round(config.db_path.stat().st_size / 1024 / 1024, 2)
|
|
1504
|
+
|
|
1505
|
+
# Open engine for counts (json branch only — LLD Decision B).
|
|
1506
|
+
# Fail-open to 0 on any error; status must never crash.
|
|
1507
|
+
# Guard on db existence: `slm status --json` must stay observational —
|
|
1508
|
+
# opening the engine on a fresh install would create + migrate the db
|
|
1509
|
+
# (MemoryEngine.initialize → DatabaseManager mkdir/connect/DDL). A
|
|
1510
|
+
# previously read-only command must not acquire a write side-effect.
|
|
1511
|
+
fact_count = 0
|
|
1512
|
+
entity_count = 0
|
|
1513
|
+
edge_count = 0
|
|
1514
|
+
eng = None
|
|
1515
|
+
if config.db_path.exists():
|
|
1516
|
+
try:
|
|
1517
|
+
from superlocalmemory.core.engine import MemoryEngine
|
|
1518
|
+
from superlocalmemory.core.engine_capabilities import Capabilities
|
|
1519
|
+
eng = MemoryEngine(config, capabilities=Capabilities.LIGHT)
|
|
1520
|
+
eng.initialize()
|
|
1521
|
+
pid = config.active_profile
|
|
1522
|
+
fact_count = eng._db.get_fact_count(pid)
|
|
1523
|
+
rows = eng._db.execute(
|
|
1524
|
+
"SELECT COUNT(*) AS c FROM canonical_entities WHERE profile_id = ?",
|
|
1525
|
+
(pid,),
|
|
1526
|
+
)
|
|
1527
|
+
entity_count = int(dict(rows[0])["c"]) if rows else 0
|
|
1528
|
+
rows2 = eng._db.execute(
|
|
1529
|
+
"SELECT COUNT(*) AS c FROM graph_edges WHERE profile_id = ?",
|
|
1530
|
+
(pid,),
|
|
1531
|
+
)
|
|
1532
|
+
edge_count = int(dict(rows2[0])["c"]) if rows2 else 0
|
|
1533
|
+
except Exception:
|
|
1534
|
+
logger.debug("cmd_status: engine count query failed; using 0s", exc_info=True)
|
|
1535
|
+
finally:
|
|
1536
|
+
if eng is not None:
|
|
1537
|
+
try:
|
|
1538
|
+
eng.close()
|
|
1539
|
+
except Exception:
|
|
1540
|
+
pass
|
|
1541
|
+
|
|
1321
1542
|
data = {
|
|
1322
1543
|
"mode": config.mode.value.upper(),
|
|
1323
1544
|
"provider": config.llm.provider or "none",
|
|
1545
|
+
"profile": config.active_profile,
|
|
1324
1546
|
"base_dir": str(config.base_dir),
|
|
1325
1547
|
"db_path": str(config.db_path),
|
|
1548
|
+
"db_size_mb": db_size_mb,
|
|
1549
|
+
"fact_count": fact_count,
|
|
1550
|
+
"entity_count": entity_count,
|
|
1551
|
+
"edge_count": edge_count,
|
|
1326
1552
|
}
|
|
1327
|
-
if config.db_path.exists():
|
|
1328
|
-
data["db_size_mb"] = round(config.db_path.stat().st_size / 1024 / 1024, 2)
|
|
1329
1553
|
json_print("status", data=data, next_actions=[
|
|
1330
1554
|
{"command": "slm health --json", "description": "Check math layer health"},
|
|
1331
1555
|
{"command": "slm list --json", "description": "List recent memories"},
|
|
@@ -1406,6 +1630,63 @@ def cmd_health(args: Namespace) -> None:
|
|
|
1406
1630
|
print(f" Mode: {config.mode.value.upper()}")
|
|
1407
1631
|
|
|
1408
1632
|
|
|
1633
|
+
def _gather_optimize_surface_b() -> dict:
|
|
1634
|
+
"""Gather Surface-B health data for slm doctor.
|
|
1635
|
+
|
|
1636
|
+
Pure data-gather — never raises, never prints, never starts the
|
|
1637
|
+
ConfigStore watchdog thread. Reads daemon-persisted metrics only
|
|
1638
|
+
(CacheDB.metrics_load), never the in-process KV counters.
|
|
1639
|
+
|
|
1640
|
+
Returns a dict with keys:
|
|
1641
|
+
enabled, cache_enabled, compress_enabled, proxy_enabled,
|
|
1642
|
+
compress_runs, tokens_saved, cache_hits, cache_misses,
|
|
1643
|
+
db_present, error
|
|
1644
|
+
"""
|
|
1645
|
+
from pathlib import Path
|
|
1646
|
+
from superlocalmemory.optimize.storage.db import CacheDB
|
|
1647
|
+
|
|
1648
|
+
result: dict = {
|
|
1649
|
+
"enabled": False,
|
|
1650
|
+
"cache_enabled": False,
|
|
1651
|
+
"compress_enabled": False,
|
|
1652
|
+
"proxy_enabled": False,
|
|
1653
|
+
"compress_runs": 0,
|
|
1654
|
+
"tokens_saved": 0,
|
|
1655
|
+
"cache_hits": 0,
|
|
1656
|
+
"cache_misses": 0,
|
|
1657
|
+
"db_present": False,
|
|
1658
|
+
"error": "",
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
# Step 1: read optimize config — NO watchdog start.
|
|
1662
|
+
try:
|
|
1663
|
+
from superlocalmemory.optimize.config.store import ConfigStore
|
|
1664
|
+
cfg = ConfigStore().get()
|
|
1665
|
+
result["enabled"] = cfg.enabled
|
|
1666
|
+
result["cache_enabled"] = cfg.cache_enabled
|
|
1667
|
+
result["compress_enabled"] = cfg.compress_enabled
|
|
1668
|
+
result["proxy_enabled"] = cfg.proxy_enabled
|
|
1669
|
+
except Exception as exc: # noqa: BLE001
|
|
1670
|
+
result["error"] = str(exc)
|
|
1671
|
+
# enabled stays False — safe default
|
|
1672
|
+
|
|
1673
|
+
# Step 2: read persisted metrics from llmcache.db (daemon-flushed, ≤60s stale).
|
|
1674
|
+
try:
|
|
1675
|
+
db_path = Path.home() / ".superlocalmemory" / "llmcache.db"
|
|
1676
|
+
result["db_present"] = db_path.exists()
|
|
1677
|
+
if result["db_present"]:
|
|
1678
|
+
snap = CacheDB.get_default().metrics_load()
|
|
1679
|
+
result["compress_runs"] = snap.compress_runs
|
|
1680
|
+
result["tokens_saved"] = snap.tokens_saved_compress
|
|
1681
|
+
result["cache_hits"] = snap.hits
|
|
1682
|
+
result["cache_misses"] = snap.misses
|
|
1683
|
+
except Exception as exc: # noqa: BLE001
|
|
1684
|
+
prior = result["error"]
|
|
1685
|
+
result["error"] = (prior + "; " if prior else "") + "metrics read failed"
|
|
1686
|
+
|
|
1687
|
+
return result
|
|
1688
|
+
|
|
1689
|
+
|
|
1409
1690
|
def cmd_doctor(args: Namespace) -> None:
|
|
1410
1691
|
"""Comprehensive pre-flight check — verify everything works.
|
|
1411
1692
|
|
|
@@ -1680,6 +1961,89 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
1680
1961
|
else:
|
|
1681
1962
|
_check("Database", "PASS", "not yet created (will initialize on first use)")
|
|
1682
1963
|
|
|
1964
|
+
# 11. PEP 668 advisory — WP-07: detect EXTERNALLY-MANAGED marker and
|
|
1965
|
+
# recommend pipx when the system Python is managed by the OS package
|
|
1966
|
+
# manager (e.g. Homebrew, Debian/Ubuntu, Fedora 38+).
|
|
1967
|
+
try:
|
|
1968
|
+
import sysconfig as _sc
|
|
1969
|
+
_stdlib = _sc.get_path("stdlib")
|
|
1970
|
+
if _stdlib:
|
|
1971
|
+
_em_marker = Path(_stdlib) / "EXTERNALLY-MANAGED"
|
|
1972
|
+
if _em_marker.exists():
|
|
1973
|
+
_check(
|
|
1974
|
+
"PEP 668 / Install method",
|
|
1975
|
+
"WARN",
|
|
1976
|
+
"System Python is externally managed (EXTERNALLY-MANAGED marker found). "
|
|
1977
|
+
"pip install may fail with PEP 668 error.",
|
|
1978
|
+
"Use pipx for an isolated install: pipx install superlocalmemory "
|
|
1979
|
+
"(last-resort only: pip install --break-system-packages superlocalmemory)",
|
|
1980
|
+
)
|
|
1981
|
+
else:
|
|
1982
|
+
_check(
|
|
1983
|
+
"PEP 668 / Install method",
|
|
1984
|
+
"PASS",
|
|
1985
|
+
"No EXTERNALLY-MANAGED marker — standard pip install supported",
|
|
1986
|
+
)
|
|
1987
|
+
except Exception:
|
|
1988
|
+
pass # advisory only — never fail doctor on this check
|
|
1989
|
+
|
|
1990
|
+
# 12. Optimize (Surface B) — reads daemon-persisted metrics (≤60s stale).
|
|
1991
|
+
info = _gather_optimize_surface_b()
|
|
1992
|
+
_enabled = info["enabled"]
|
|
1993
|
+
_error = info.get("error", "")
|
|
1994
|
+
if not _enabled:
|
|
1995
|
+
_check(
|
|
1996
|
+
"Optimize (Surface B)",
|
|
1997
|
+
"WARN",
|
|
1998
|
+
"disabled (optimize.json enabled=false) — caching/compression not active"
|
|
1999
|
+
+ (f" [{_error}]" if _error else ""),
|
|
2000
|
+
fix="Enable via dashboard Optimize tab or set enabled=true"
|
|
2001
|
+
" in ~/.superlocalmemory/optimize.json",
|
|
2002
|
+
)
|
|
2003
|
+
else:
|
|
2004
|
+
_surfaces = []
|
|
2005
|
+
if info["cache_enabled"]:
|
|
2006
|
+
_surfaces.append("cache")
|
|
2007
|
+
if info["compress_enabled"]:
|
|
2008
|
+
_surfaces.append("compress")
|
|
2009
|
+
if info["proxy_enabled"]:
|
|
2010
|
+
_surfaces.append("proxy")
|
|
2011
|
+
_stats = (
|
|
2012
|
+
f"compress_runs={info['compress_runs']}"
|
|
2013
|
+
f" tokens_saved={info['tokens_saved']}"
|
|
2014
|
+
f" cache_hits={info['cache_hits']}"
|
|
2015
|
+
f" cache_misses={info['cache_misses']}"
|
|
2016
|
+
)
|
|
2017
|
+
_surface_str = ",".join(_surfaces) if _surfaces else "(none)"
|
|
2018
|
+
if not _surfaces:
|
|
2019
|
+
_check(
|
|
2020
|
+
"Optimize (Surface B)",
|
|
2021
|
+
"WARN",
|
|
2022
|
+
f"enabled [{_surface_str}] but no surface active"
|
|
2023
|
+
+ (f" [{_error}]" if _error else ""),
|
|
2024
|
+
)
|
|
2025
|
+
elif not info["db_present"]:
|
|
2026
|
+
_check(
|
|
2027
|
+
"Optimize (Surface B)",
|
|
2028
|
+
"WARN",
|
|
2029
|
+
f"enabled [{_surface_str}] {_stats}"
|
|
2030
|
+
" but no metrics yet (llmcache.db not created)"
|
|
2031
|
+
+ (f" [{_error}]" if _error else ""),
|
|
2032
|
+
fix="slm serve start",
|
|
2033
|
+
)
|
|
2034
|
+
elif _error:
|
|
2035
|
+
_check(
|
|
2036
|
+
"Optimize (Surface B)",
|
|
2037
|
+
"WARN",
|
|
2038
|
+
f"enabled [{_surface_str}] {_stats} (partial: {_error})",
|
|
2039
|
+
)
|
|
2040
|
+
else:
|
|
2041
|
+
_check(
|
|
2042
|
+
"Optimize (Surface B)",
|
|
2043
|
+
"PASS",
|
|
2044
|
+
f"enabled [{_surface_str}] {_stats}",
|
|
2045
|
+
)
|
|
2046
|
+
|
|
1683
2047
|
# Summary
|
|
1684
2048
|
if use_json:
|
|
1685
2049
|
from superlocalmemory.cli.json_output import json_print
|
|
@@ -2003,14 +2367,82 @@ def cmd_profile(args: Namespace) -> None:
|
|
|
2003
2367
|
# -- Active Memory commands (V3.1) ------------------------------------------
|
|
2004
2368
|
|
|
2005
2369
|
|
|
2370
|
+
def _cmd_init_auto(
|
|
2371
|
+
args: Namespace,
|
|
2372
|
+
slm_data_dir: "Path",
|
|
2373
|
+
config_exists: bool,
|
|
2374
|
+
force: bool,
|
|
2375
|
+
) -> None:
|
|
2376
|
+
"""WP-07: non-interactive --auto branch for slm init.
|
|
2377
|
+
|
|
2378
|
+
Best-effort at every step; only exits non-zero when config save fails.
|
|
2379
|
+
No TTY required. Does NOT run IDE connect (AC6).
|
|
2380
|
+
"""
|
|
2381
|
+
from pathlib import Path
|
|
2382
|
+
from superlocalmemory.core.config import SLMConfig
|
|
2383
|
+
from superlocalmemory.storage.models import Mode
|
|
2384
|
+
from superlocalmemory.cli.setup_wizard import _mark_complete
|
|
2385
|
+
|
|
2386
|
+
# Step 1: write mode-A config (create-if-absent or --force).
|
|
2387
|
+
# Pass slm_data_dir explicitly so env-overridden paths are respected even
|
|
2388
|
+
# when DEFAULT_BASE_DIR was evaluated before the env var was set (e.g. tests).
|
|
2389
|
+
if force or not config_exists:
|
|
2390
|
+
try:
|
|
2391
|
+
cfg = SLMConfig.for_mode(Mode.A, base_dir=slm_data_dir)
|
|
2392
|
+
cfg.save(mode_change=True)
|
|
2393
|
+
except Exception as exc:
|
|
2394
|
+
print(f"[ERROR] slm init --auto: config save failed: {exc}", file=sys.stderr)
|
|
2395
|
+
sys.exit(1)
|
|
2396
|
+
|
|
2397
|
+
# Step 2: mark complete (write .setup-complete sentinel).
|
|
2398
|
+
# Write the sentinel directly using slm_data_dir to avoid the module-level
|
|
2399
|
+
# _SLM_HOME resolved at import time (tests set the env var after import).
|
|
2400
|
+
try:
|
|
2401
|
+
import platform
|
|
2402
|
+
import time as _time
|
|
2403
|
+
sentinel = slm_data_dir / ".setup-complete"
|
|
2404
|
+
slm_data_dir.mkdir(parents=True, exist_ok=True)
|
|
2405
|
+
sentinel.write_text(
|
|
2406
|
+
f"setup_completed={_time.strftime('%Y-%m-%dT%H:%M:%S')}\n"
|
|
2407
|
+
f"python={sys.executable}\n"
|
|
2408
|
+
f"platform={platform.system()}\n"
|
|
2409
|
+
f"version={platform.python_version()}\n"
|
|
2410
|
+
)
|
|
2411
|
+
except Exception:
|
|
2412
|
+
pass # best-effort — sentinel is advisory only
|
|
2413
|
+
|
|
2414
|
+
# Step 3: install hooks if ~/.claude exists.
|
|
2415
|
+
try:
|
|
2416
|
+
claude_dir = Path.home() / ".claude"
|
|
2417
|
+
if claude_dir.exists():
|
|
2418
|
+
from superlocalmemory.hooks.claude_code_hooks import install_hooks
|
|
2419
|
+
install_hooks(include_gate=getattr(args, "gate", False))
|
|
2420
|
+
except Exception:
|
|
2421
|
+
pass # best-effort
|
|
2422
|
+
|
|
2423
|
+
# Step 4: warmup best-effort (don't block if models not present).
|
|
2424
|
+
# Skipped in --auto to keep startup fast for CI; user can run slm warmup.
|
|
2425
|
+
|
|
2426
|
+
print("[OK] slm init --auto: setup complete (mode A, non-interactive)", file=sys.stderr)
|
|
2427
|
+
|
|
2428
|
+
|
|
2006
2429
|
def cmd_init(args: Namespace) -> None:
|
|
2007
2430
|
"""One-command setup: mode + hooks + IDE connect + warmup."""
|
|
2008
2431
|
from pathlib import Path
|
|
2432
|
+
from superlocalmemory.cli._lazy_init import slm_home
|
|
2009
2433
|
from superlocalmemory.core.config import SLMConfig
|
|
2010
2434
|
|
|
2011
2435
|
force = getattr(args, "force", False)
|
|
2436
|
+
auto = getattr(args, "auto", False)
|
|
2437
|
+
|
|
2438
|
+
slm_data_dir = slm_home()
|
|
2439
|
+
config_exists = (slm_data_dir / "config.json").exists()
|
|
2012
2440
|
|
|
2013
|
-
|
|
2441
|
+
# WP-07: --auto branch — fully non-interactive, no TTY required (AC6).
|
|
2442
|
+
if auto:
|
|
2443
|
+
os.environ["SLM_NON_INTERACTIVE"] = "1"
|
|
2444
|
+
_cmd_init_auto(args, slm_data_dir, config_exists, force)
|
|
2445
|
+
return
|
|
2014
2446
|
|
|
2015
2447
|
print()
|
|
2016
2448
|
print("SuperLocalMemory — One-Time Setup")
|
|
@@ -2114,41 +2546,7 @@ def cmd_hooks(args: Namespace) -> None:
|
|
|
2114
2546
|
if include_gate:
|
|
2115
2547
|
print(" Gate: ON (enforces session_init — experimental)")
|
|
2116
2548
|
print(" SLM: Hooks installed into Claude Code (slm hooks remove to undo)")
|
|
2117
|
-
|
|
2118
|
-
# etc. are available immediately in Claude Code regardless of
|
|
2119
|
-
# whether the user installed via npm or pip.
|
|
2120
|
-
try:
|
|
2121
|
-
import importlib.resources as _ir
|
|
2122
|
-
import importlib.util as _iu
|
|
2123
|
-
import shutil as _sh
|
|
2124
|
-
from pathlib import Path as _P
|
|
2125
|
-
|
|
2126
|
-
claude_skills_dir = _P.home() / ".claude" / "skills"
|
|
2127
|
-
claude_skills_dir.mkdir(parents=True, exist_ok=True)
|
|
2128
|
-
|
|
2129
|
-
# Try Python-package bundled skills first (works for both
|
|
2130
|
-
# pip and npm users who have the Python pkg installed).
|
|
2131
|
-
pkg_skills: _P | None = None
|
|
2132
|
-
spec = _iu.find_spec("superlocalmemory")
|
|
2133
|
-
if spec and spec.submodule_search_locations:
|
|
2134
|
-
candidate = _P(list(spec.submodule_search_locations)[0]) / "skills"
|
|
2135
|
-
if candidate.is_dir():
|
|
2136
|
-
pkg_skills = candidate
|
|
2137
|
-
|
|
2138
|
-
if pkg_skills:
|
|
2139
|
-
installed_skills = 0
|
|
2140
|
-
for d in pkg_skills.iterdir():
|
|
2141
|
-
if d.is_dir():
|
|
2142
|
-
src_skill = d / "SKILL.md"
|
|
2143
|
-
if src_skill.exists():
|
|
2144
|
-
dst = claude_skills_dir / (d.name + ".md")
|
|
2145
|
-
_sh.copy2(str(src_skill), str(dst))
|
|
2146
|
-
installed_skills += 1
|
|
2147
|
-
if installed_skills:
|
|
2148
|
-
print(f" Skills: {installed_skills} skills installed → {claude_skills_dir}")
|
|
2149
|
-
print(" Use /slm-recall, /slm-remember, /slm-status in Claude Code")
|
|
2150
|
-
except Exception as _skill_exc:
|
|
2151
|
-
pass # non-fatal — skills can be installed via install-skills.sh
|
|
2549
|
+
|
|
2152
2550
|
else:
|
|
2153
2551
|
print(f"Installation failed: {result['errors']}")
|
|
2154
2552
|
elif action == "remove":
|