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.
Files changed (147) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/CHANGELOG.md +28 -0
  3. package/README.md +189 -740
  4. package/package.json +12 -5
  5. package/plugin/.claude-plugin/plugin.json +20 -0
  6. package/plugin/.mcp.json +12 -0
  7. package/plugin/CLAUDE.md +44 -0
  8. package/plugin/_GENERATED.md +6 -0
  9. package/plugin/agents/slm-memory-advisor.md +44 -0
  10. package/plugin/agents/slm-optimize-advisor.md +38 -0
  11. package/plugin/hooks/hooks.json +14 -0
  12. package/plugin/requirements.txt +1 -0
  13. package/plugin/scripts/ensure-venv.bat +122 -0
  14. package/plugin/scripts/ensure-venv.sh +105 -0
  15. package/plugin/scripts/slm-launch +15 -0
  16. package/plugin/scripts/slm-launch.bat +17 -0
  17. package/plugin/settings.json +16 -0
  18. package/plugin/skills/slm-cache/SKILL.md +140 -0
  19. package/plugin/skills/slm-compress/SKILL.md +143 -0
  20. package/plugin/skills/slm-graph/SKILL.md +300 -0
  21. package/plugin/skills/slm-recall/SKILL.md +204 -0
  22. package/plugin/skills/slm-remember/SKILL.md +194 -0
  23. package/plugin/skills/slm-session/SKILL.md +207 -0
  24. package/plugin/skills/slm-status/SKILL.md +149 -0
  25. package/plugin-src/.mcp.json +12 -0
  26. package/plugin-src/agents/slm-memory-advisor.md +44 -0
  27. package/plugin-src/agents/slm-optimize-advisor.md +38 -0
  28. package/plugin-src/commands/slm-optimize.md +22 -0
  29. package/plugin-src/commands/slm-recall.md +16 -0
  30. package/plugin-src/commands/slm-remember.md +16 -0
  31. package/plugin-src/commands/slm-status.md +15 -0
  32. package/plugin-src/hooks/.gitkeep +0 -0
  33. package/plugin-src/hooks/hooks.json +14 -0
  34. package/plugin-src/manifest.json +25 -0
  35. package/plugin-src/requirements.txt +1 -0
  36. package/plugin-src/rules/AGENTS.md +91 -0
  37. package/plugin-src/rules/CLAUDE.md.fragment +44 -0
  38. package/plugin-src/scripts/ensure-venv.bat +122 -0
  39. package/plugin-src/scripts/ensure-venv.sh +105 -0
  40. package/plugin-src/scripts/slm-launch +15 -0
  41. package/plugin-src/scripts/slm-launch.bat +17 -0
  42. package/plugin-src/settings.json +16 -0
  43. package/plugin-src/skills/slm-cache/SKILL.md +140 -0
  44. package/plugin-src/skills/slm-compress/SKILL.md +143 -0
  45. package/plugin-src/skills/slm-graph/SKILL.md +300 -0
  46. package/plugin-src/skills/slm-recall/SKILL.md +204 -0
  47. package/plugin-src/skills/slm-remember/SKILL.md +194 -0
  48. package/plugin-src/skills/slm-session/SKILL.md +207 -0
  49. package/plugin-src/skills/slm-status/SKILL.md +149 -0
  50. package/pyproject.toml +6 -2
  51. package/scripts/__tests__/build-plugin.test.mjs +613 -0
  52. package/scripts/_savings_math.py +270 -0
  53. package/scripts/build-plugin.js +742 -0
  54. package/scripts/dogfood_savings.py +490 -0
  55. package/scripts/install-skills.ps1 +4 -334
  56. package/scripts/install-skills.sh +4 -435
  57. package/scripts/postinstall-interactive.js +0 -27
  58. package/scripts/postinstall.js +21 -2
  59. package/src/superlocalmemory/__init__.py +1 -1
  60. package/src/superlocalmemory/cli/_lazy_init.py +115 -0
  61. package/src/superlocalmemory/cli/commands.py +439 -41
  62. package/src/superlocalmemory/cli/main.py +92 -4
  63. package/src/superlocalmemory/cli/setup_wizard.py +47 -6
  64. package/src/superlocalmemory/core/backend_orchestrator.py +12 -8
  65. package/src/superlocalmemory/core/config.py +194 -9
  66. package/src/superlocalmemory/core/embeddings.py +10 -5
  67. package/src/superlocalmemory/core/engine.py +76 -5
  68. package/src/superlocalmemory/core/fact_consolidator.py +20 -3
  69. package/src/superlocalmemory/core/platform_utils.py +8 -0
  70. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  71. package/src/superlocalmemory/core/recall_worker.py +7 -0
  72. package/src/superlocalmemory/core/store_pipeline.py +23 -1
  73. package/src/superlocalmemory/core/worker_pool.py +14 -2
  74. package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
  75. package/src/superlocalmemory/hooks/portable_kit.py +506 -0
  76. package/src/superlocalmemory/hooks/session_registry.py +8 -4
  77. package/src/superlocalmemory/infra/cloud_backup.py +99 -23
  78. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -2
  79. package/src/superlocalmemory/mcp/_pool_adapter.py +15 -6
  80. package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
  81. package/src/superlocalmemory/mcp/server.py +75 -4
  82. package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
  83. package/src/superlocalmemory/mcp/tools_core.py +37 -4
  84. package/src/superlocalmemory/mcp/tools_v3.py +6 -1
  85. package/src/superlocalmemory/mcp/tools_v33.py +8 -4
  86. package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
  87. package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
  88. package/src/superlocalmemory/optimize/cache/manager.py +92 -6
  89. package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
  90. package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
  91. package/src/superlocalmemory/optimize/compress/router.py +46 -13
  92. package/src/superlocalmemory/optimize/config/schema.py +6 -0
  93. package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
  94. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
  95. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
  96. package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
  97. package/src/superlocalmemory/optimize/proxy/server.py +11 -0
  98. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
  99. package/src/superlocalmemory/optimize/storage/db.py +30 -0
  100. package/src/superlocalmemory/retrieval/bm25_channel.py +12 -2
  101. package/src/superlocalmemory/retrieval/engine.py +36 -3
  102. package/src/superlocalmemory/retrieval/entity_channel.py +5 -5
  103. package/src/superlocalmemory/retrieval/hopfield_channel.py +10 -2
  104. package/src/superlocalmemory/retrieval/semantic_channel.py +10 -2
  105. package/src/superlocalmemory/server/recall_serializer.py +3 -1
  106. package/src/superlocalmemory/server/unified_daemon.py +156 -16
  107. package/src/superlocalmemory/storage/database.py +215 -43
  108. package/src/superlocalmemory/storage/migration_runner.py +17 -1
  109. package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +120 -0
  110. package/src/superlocalmemory/storage/models.py +10 -0
  111. package/src/superlocalmemory/storage/schema.py +15 -10
  112. package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
  113. package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
  114. package/src/superlocalmemory/ui/index.html +2 -2
  115. package/src/superlocalmemory/ui/js/core.js +98 -0
  116. package/src/superlocalmemory/ui/js/dashboard.js +8 -1
  117. package/src/superlocalmemory/ui/js/ide-status.js +16 -3
  118. package/src/superlocalmemory/ui/js/math-health.js +15 -3
  119. package/src/superlocalmemory/ui/js/optimize.js +18 -2
  120. package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
  121. package/src/superlocalmemory.egg-info/PKG-INFO +191 -741
  122. package/src/superlocalmemory.egg-info/SOURCES.txt +7 -9
  123. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  124. package/ide/skills/slm-build-graph/SKILL.md +0 -423
  125. package/ide/skills/slm-list-recent/SKILL.md +0 -348
  126. package/ide/skills/slm-recall/SKILL.md +0 -326
  127. package/ide/skills/slm-remember/SKILL.md +0 -194
  128. package/ide/skills/slm-show-patterns/SKILL.md +0 -224
  129. package/ide/skills/slm-status/SKILL.md +0 -363
  130. package/ide/skills/slm-switch-profile/SKILL.md +0 -442
  131. package/skills/slm-build-graph/SKILL.md +0 -423
  132. package/skills/slm-list-recent/SKILL.md +0 -348
  133. package/skills/slm-optimize/README.md +0 -55
  134. package/skills/slm-optimize/SKILL.md +0 -139
  135. package/skills/slm-recall/SKILL.md +0 -343
  136. package/skills/slm-remember/SKILL.md +0 -194
  137. package/skills/slm-show-patterns/SKILL.md +0 -224
  138. package/skills/slm-status/SKILL.md +0 -363
  139. package/skills/slm-switch-profile/SKILL.md +0 -442
  140. package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
  141. package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
  142. package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
  143. package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
  144. package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
  145. package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
  146. package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
  147. 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. V3.4.22: ``--cross-platform`` uses LLD-05."""
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(args.content, metadata=metadata)
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
- config_exists = (Path.home() / ".superlocalmemory" / "config.json").exists()
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
- # S9-DASH-11: also install skills so /slm-recall, /slm-remember
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":