superlocalmemory 3.6.12 → 3.6.14

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 (126) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +187 -741
  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 +43 -0
  8. package/plugin/_GENERATED.md +6 -0
  9. package/plugin/agents/slm-memory-advisor.md +43 -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 +196 -0
  22. package/plugin/skills/slm-remember/SKILL.md +182 -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 +43 -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 +90 -0
  37. package/plugin-src/rules/CLAUDE.md.fragment +43 -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 +196 -0
  47. package/plugin-src/skills/slm-remember/SKILL.md +182 -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 +348 -39
  62. package/src/superlocalmemory/cli/main.py +81 -25
  63. package/src/superlocalmemory/cli/setup_wizard.py +20 -6
  64. package/src/superlocalmemory/cli/version_banner.py +6 -2
  65. package/src/superlocalmemory/core/config.py +79 -9
  66. package/src/superlocalmemory/core/embeddings.py +10 -5
  67. package/src/superlocalmemory/core/engine.py +2 -2
  68. package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
  69. package/src/superlocalmemory/hooks/portable_kit.py +506 -0
  70. package/src/superlocalmemory/infra/cloud_backup.py +99 -23
  71. package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
  72. package/src/superlocalmemory/mcp/server.py +75 -4
  73. package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
  74. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  75. package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
  76. package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
  77. package/src/superlocalmemory/optimize/cache/manager.py +92 -6
  78. package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
  79. package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
  80. package/src/superlocalmemory/optimize/compress/router.py +46 -13
  81. package/src/superlocalmemory/optimize/config/schema.py +6 -0
  82. package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
  83. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
  84. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
  85. package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
  86. package/src/superlocalmemory/optimize/proxy/server.py +11 -0
  87. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
  88. package/src/superlocalmemory/optimize/storage/db.py +30 -0
  89. package/src/superlocalmemory/server/recall_serializer.py +3 -1
  90. package/src/superlocalmemory/server/unified_daemon.py +24 -6
  91. package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
  92. package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
  93. package/src/superlocalmemory/ui/index.html +2 -2
  94. package/src/superlocalmemory/ui/js/core.js +98 -0
  95. package/src/superlocalmemory/ui/js/dashboard.js +8 -1
  96. package/src/superlocalmemory/ui/js/ide-status.js +16 -3
  97. package/src/superlocalmemory/ui/js/math-health.js +15 -3
  98. package/src/superlocalmemory/ui/js/optimize.js +18 -2
  99. package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
  100. package/src/superlocalmemory.egg-info/PKG-INFO +189 -742
  101. package/src/superlocalmemory.egg-info/SOURCES.txt +6 -9
  102. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  103. package/ide/skills/slm-build-graph/SKILL.md +0 -423
  104. package/ide/skills/slm-list-recent/SKILL.md +0 -348
  105. package/ide/skills/slm-recall/SKILL.md +0 -326
  106. package/ide/skills/slm-remember/SKILL.md +0 -194
  107. package/ide/skills/slm-show-patterns/SKILL.md +0 -224
  108. package/ide/skills/slm-status/SKILL.md +0 -363
  109. package/ide/skills/slm-switch-profile/SKILL.md +0 -442
  110. package/skills/slm-build-graph/SKILL.md +0 -423
  111. package/skills/slm-list-recent/SKILL.md +0 -348
  112. package/skills/slm-optimize/README.md +0 -55
  113. package/skills/slm-optimize/SKILL.md +0 -139
  114. package/skills/slm-recall/SKILL.md +0 -343
  115. package/skills/slm-remember/SKILL.md +0 -194
  116. package/skills/slm-show-patterns/SKILL.md +0 -224
  117. package/skills/slm-status/SKILL.md +0 -363
  118. package/skills/slm-switch-profile/SKILL.md +0 -442
  119. package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
  120. package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
  121. package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
  122. package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
  123. package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
  124. package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
  125. package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
  126. package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +0 -442
@@ -780,8 +780,97 @@ def _cmd_context_dispatch(args: Namespace) -> None:
780
780
  cmd_context(args)
781
781
 
782
782
 
783
+ def _agents_md_source_factory():
784
+ """Return a callable that reads the WP-05 AGENTS.md content, or None on failure.
785
+
786
+ Source: plugin-src/rules/AGENTS.md (relative to package root).
787
+ Gracefully skips if absent — never fails the MCP write.
788
+ """
789
+ from pathlib import Path
790
+
791
+ # Resolve relative to the package root (src/superlocalmemory/../../)
792
+ _pkg_root = Path(__file__).resolve().parents[3]
793
+ _agents_src = _pkg_root / "plugin-src" / "rules" / "AGENTS.md"
794
+
795
+ def _read() -> str | None:
796
+ if _agents_src.exists():
797
+ return _agents_src.read_text(encoding="utf-8")
798
+ logger.warning(
799
+ "WP-05 AGENTS.md not found at %s — skipping AGENTS.md write", _agents_src
800
+ )
801
+ return None
802
+
803
+ return _read
804
+
805
+
783
806
  def cmd_connect(args: Namespace) -> None:
784
- """Configure IDE integrations. V3.4.22: ``--cross-platform`` uses LLD-05."""
807
+ """Configure IDE integrations.
808
+
809
+ Dispatch priority (WP-08):
810
+ 1. ``slm connect <ide>`` where ide ∈ IDE_MATRIX → portable_kit.connect_ide
811
+ (MCP-wiring + AGENTS.md; includes claude-code short-circuit to WP-06).
812
+ 2. ``--cross-platform`` / ``--disable`` → LLD-05 CrossPlatformConnector.
813
+ 3. Bare ``slm connect`` / ``--list`` → legacy IDEConnector (markdown-rules).
814
+ """
815
+ ide_arg = getattr(args, "ide", None)
816
+
817
+ # WP-08: intercept known IDE_MATRIX ids before legacy branches (CRIT-1)
818
+ if ide_arg is not None:
819
+ from superlocalmemory.hooks.portable_kit import (
820
+ IDE_MATRIX,
821
+ connect_ide,
822
+ supported_ides,
823
+ )
824
+
825
+ if ide_arg in IDE_MATRIX:
826
+ here = getattr(args, "here", False)
827
+ profile = getattr(args, "profile", None)
828
+ project = None
829
+ if here:
830
+ import pathlib
831
+ project = pathlib.Path.cwd()
832
+
833
+ result = connect_ide(
834
+ ide_arg,
835
+ home=None,
836
+ project=project,
837
+ here=here,
838
+ profile=profile,
839
+ agents_md_source=_agents_md_source_factory(),
840
+ )
841
+
842
+ if getattr(args, "json", False):
843
+ from superlocalmemory.cli.json_output import json_print
844
+ json_print("connect", data=result)
845
+ return
846
+
847
+ if result["error"]:
848
+ print(f"Error: {result['error']}", file=sys.stderr)
849
+ print(
850
+ f"Supported IDEs: {', '.join(supported_ides())}",
851
+ file=sys.stderr,
852
+ )
853
+ sys.exit(1)
854
+
855
+ status_sym = {"wrote": "[+]", "merged": "[~]", "unchanged": "[=]",
856
+ "skipped": "[s]", "error": "[!]"}.get(
857
+ result["mcp_config"], "[?]"
858
+ )
859
+ print(
860
+ f"{status_sym} {ide_arg}: mcp_config={result['mcp_config']} "
861
+ f"path={result['mcp_path']}"
862
+ )
863
+ print(f" agents_md={result['agents_md']}")
864
+ return
865
+
866
+ # Unknown ide — list supported and exit non-zero
867
+ from superlocalmemory.hooks.portable_kit import supported_ides
868
+ print(
869
+ f"Unknown IDE '{ide_arg}'.\nSupported: {', '.join(supported_ides())}",
870
+ file=sys.stderr,
871
+ )
872
+ sys.exit(1)
873
+
785
874
  # Route --disable <name> and --cross-platform to the LLD-05 orchestrator.
786
875
  if getattr(args, "disable", None) or getattr(args, "cross_platform", False):
787
876
  from superlocalmemory.cli.context_commands import (
@@ -1318,14 +1407,60 @@ def cmd_status(args: Namespace) -> None:
1318
1407
 
1319
1408
  if getattr(args, 'json', False):
1320
1409
  from superlocalmemory.cli.json_output import json_print
1410
+
1411
+ # WP-02 D8: canonical key set — db_size_mb always present (0.0 if absent).
1412
+ db_size_mb = 0.0
1413
+ if config.db_path.exists():
1414
+ db_size_mb = round(config.db_path.stat().st_size / 1024 / 1024, 2)
1415
+
1416
+ # Open engine for counts (json branch only — LLD Decision B).
1417
+ # Fail-open to 0 on any error; status must never crash.
1418
+ # Guard on db existence: `slm status --json` must stay observational —
1419
+ # opening the engine on a fresh install would create + migrate the db
1420
+ # (MemoryEngine.initialize → DatabaseManager mkdir/connect/DDL). A
1421
+ # previously read-only command must not acquire a write side-effect.
1422
+ fact_count = 0
1423
+ entity_count = 0
1424
+ edge_count = 0
1425
+ eng = None
1426
+ if config.db_path.exists():
1427
+ try:
1428
+ from superlocalmemory.core.engine import MemoryEngine
1429
+ from superlocalmemory.core.engine_capabilities import Capabilities
1430
+ eng = MemoryEngine(config, capabilities=Capabilities.LIGHT)
1431
+ eng.initialize()
1432
+ pid = config.active_profile
1433
+ fact_count = eng._db.get_fact_count(pid)
1434
+ rows = eng._db.execute(
1435
+ "SELECT COUNT(*) AS c FROM canonical_entities WHERE profile_id = ?",
1436
+ (pid,),
1437
+ )
1438
+ entity_count = int(dict(rows[0])["c"]) if rows else 0
1439
+ rows2 = eng._db.execute(
1440
+ "SELECT COUNT(*) AS c FROM graph_edges WHERE profile_id = ?",
1441
+ (pid,),
1442
+ )
1443
+ edge_count = int(dict(rows2[0])["c"]) if rows2 else 0
1444
+ except Exception:
1445
+ logger.debug("cmd_status: engine count query failed; using 0s", exc_info=True)
1446
+ finally:
1447
+ if eng is not None:
1448
+ try:
1449
+ eng.close()
1450
+ except Exception:
1451
+ pass
1452
+
1321
1453
  data = {
1322
1454
  "mode": config.mode.value.upper(),
1323
1455
  "provider": config.llm.provider or "none",
1456
+ "profile": config.active_profile,
1324
1457
  "base_dir": str(config.base_dir),
1325
1458
  "db_path": str(config.db_path),
1459
+ "db_size_mb": db_size_mb,
1460
+ "fact_count": fact_count,
1461
+ "entity_count": entity_count,
1462
+ "edge_count": edge_count,
1326
1463
  }
1327
- if config.db_path.exists():
1328
- data["db_size_mb"] = round(config.db_path.stat().st_size / 1024 / 1024, 2)
1329
1464
  json_print("status", data=data, next_actions=[
1330
1465
  {"command": "slm health --json", "description": "Check math layer health"},
1331
1466
  {"command": "slm list --json", "description": "List recent memories"},
@@ -1406,6 +1541,63 @@ def cmd_health(args: Namespace) -> None:
1406
1541
  print(f" Mode: {config.mode.value.upper()}")
1407
1542
 
1408
1543
 
1544
+ def _gather_optimize_surface_b() -> dict:
1545
+ """Gather Surface-B health data for slm doctor.
1546
+
1547
+ Pure data-gather — never raises, never prints, never starts the
1548
+ ConfigStore watchdog thread. Reads daemon-persisted metrics only
1549
+ (CacheDB.metrics_load), never the in-process KV counters.
1550
+
1551
+ Returns a dict with keys:
1552
+ enabled, cache_enabled, compress_enabled, proxy_enabled,
1553
+ compress_runs, tokens_saved, cache_hits, cache_misses,
1554
+ db_present, error
1555
+ """
1556
+ from pathlib import Path
1557
+ from superlocalmemory.optimize.storage.db import CacheDB
1558
+
1559
+ result: dict = {
1560
+ "enabled": False,
1561
+ "cache_enabled": False,
1562
+ "compress_enabled": False,
1563
+ "proxy_enabled": False,
1564
+ "compress_runs": 0,
1565
+ "tokens_saved": 0,
1566
+ "cache_hits": 0,
1567
+ "cache_misses": 0,
1568
+ "db_present": False,
1569
+ "error": "",
1570
+ }
1571
+
1572
+ # Step 1: read optimize config — NO watchdog start.
1573
+ try:
1574
+ from superlocalmemory.optimize.config.store import ConfigStore
1575
+ cfg = ConfigStore().get()
1576
+ result["enabled"] = cfg.enabled
1577
+ result["cache_enabled"] = cfg.cache_enabled
1578
+ result["compress_enabled"] = cfg.compress_enabled
1579
+ result["proxy_enabled"] = cfg.proxy_enabled
1580
+ except Exception as exc: # noqa: BLE001
1581
+ result["error"] = str(exc)
1582
+ # enabled stays False — safe default
1583
+
1584
+ # Step 2: read persisted metrics from llmcache.db (daemon-flushed, ≤60s stale).
1585
+ try:
1586
+ db_path = Path.home() / ".superlocalmemory" / "llmcache.db"
1587
+ result["db_present"] = db_path.exists()
1588
+ if result["db_present"]:
1589
+ snap = CacheDB.get_default().metrics_load()
1590
+ result["compress_runs"] = snap.compress_runs
1591
+ result["tokens_saved"] = snap.tokens_saved_compress
1592
+ result["cache_hits"] = snap.hits
1593
+ result["cache_misses"] = snap.misses
1594
+ except Exception as exc: # noqa: BLE001
1595
+ prior = result["error"]
1596
+ result["error"] = (prior + "; " if prior else "") + "metrics read failed"
1597
+
1598
+ return result
1599
+
1600
+
1409
1601
  def cmd_doctor(args: Namespace) -> None:
1410
1602
  """Comprehensive pre-flight check — verify everything works.
1411
1603
 
@@ -1680,6 +1872,89 @@ def cmd_doctor(args: Namespace) -> None:
1680
1872
  else:
1681
1873
  _check("Database", "PASS", "not yet created (will initialize on first use)")
1682
1874
 
1875
+ # 11. PEP 668 advisory — WP-07: detect EXTERNALLY-MANAGED marker and
1876
+ # recommend pipx when the system Python is managed by the OS package
1877
+ # manager (e.g. Homebrew, Debian/Ubuntu, Fedora 38+).
1878
+ try:
1879
+ import sysconfig as _sc
1880
+ _stdlib = _sc.get_path("stdlib")
1881
+ if _stdlib:
1882
+ _em_marker = Path(_stdlib) / "EXTERNALLY-MANAGED"
1883
+ if _em_marker.exists():
1884
+ _check(
1885
+ "PEP 668 / Install method",
1886
+ "WARN",
1887
+ "System Python is externally managed (EXTERNALLY-MANAGED marker found). "
1888
+ "pip install may fail with PEP 668 error.",
1889
+ "Use pipx for an isolated install: pipx install superlocalmemory "
1890
+ "(last-resort only: pip install --break-system-packages superlocalmemory)",
1891
+ )
1892
+ else:
1893
+ _check(
1894
+ "PEP 668 / Install method",
1895
+ "PASS",
1896
+ "No EXTERNALLY-MANAGED marker — standard pip install supported",
1897
+ )
1898
+ except Exception:
1899
+ pass # advisory only — never fail doctor on this check
1900
+
1901
+ # 12. Optimize (Surface B) — reads daemon-persisted metrics (≤60s stale).
1902
+ info = _gather_optimize_surface_b()
1903
+ _enabled = info["enabled"]
1904
+ _error = info.get("error", "")
1905
+ if not _enabled:
1906
+ _check(
1907
+ "Optimize (Surface B)",
1908
+ "WARN",
1909
+ "disabled (optimize.json enabled=false) — caching/compression not active"
1910
+ + (f" [{_error}]" if _error else ""),
1911
+ fix="Enable via dashboard Optimize tab or set enabled=true"
1912
+ " in ~/.superlocalmemory/optimize.json",
1913
+ )
1914
+ else:
1915
+ _surfaces = []
1916
+ if info["cache_enabled"]:
1917
+ _surfaces.append("cache")
1918
+ if info["compress_enabled"]:
1919
+ _surfaces.append("compress")
1920
+ if info["proxy_enabled"]:
1921
+ _surfaces.append("proxy")
1922
+ _stats = (
1923
+ f"compress_runs={info['compress_runs']}"
1924
+ f" tokens_saved={info['tokens_saved']}"
1925
+ f" cache_hits={info['cache_hits']}"
1926
+ f" cache_misses={info['cache_misses']}"
1927
+ )
1928
+ _surface_str = ",".join(_surfaces) if _surfaces else "(none)"
1929
+ if not _surfaces:
1930
+ _check(
1931
+ "Optimize (Surface B)",
1932
+ "WARN",
1933
+ f"enabled [{_surface_str}] but no surface active"
1934
+ + (f" [{_error}]" if _error else ""),
1935
+ )
1936
+ elif not info["db_present"]:
1937
+ _check(
1938
+ "Optimize (Surface B)",
1939
+ "WARN",
1940
+ f"enabled [{_surface_str}] {_stats}"
1941
+ " but no metrics yet (llmcache.db not created)"
1942
+ + (f" [{_error}]" if _error else ""),
1943
+ fix="slm serve start",
1944
+ )
1945
+ elif _error:
1946
+ _check(
1947
+ "Optimize (Surface B)",
1948
+ "WARN",
1949
+ f"enabled [{_surface_str}] {_stats} (partial: {_error})",
1950
+ )
1951
+ else:
1952
+ _check(
1953
+ "Optimize (Surface B)",
1954
+ "PASS",
1955
+ f"enabled [{_surface_str}] {_stats}",
1956
+ )
1957
+
1683
1958
  # Summary
1684
1959
  if use_json:
1685
1960
  from superlocalmemory.cli.json_output import json_print
@@ -2003,14 +2278,82 @@ def cmd_profile(args: Namespace) -> None:
2003
2278
  # -- Active Memory commands (V3.1) ------------------------------------------
2004
2279
 
2005
2280
 
2281
+ def _cmd_init_auto(
2282
+ args: Namespace,
2283
+ slm_data_dir: "Path",
2284
+ config_exists: bool,
2285
+ force: bool,
2286
+ ) -> None:
2287
+ """WP-07: non-interactive --auto branch for slm init.
2288
+
2289
+ Best-effort at every step; only exits non-zero when config save fails.
2290
+ No TTY required. Does NOT run IDE connect (AC6).
2291
+ """
2292
+ from pathlib import Path
2293
+ from superlocalmemory.core.config import SLMConfig
2294
+ from superlocalmemory.storage.models import Mode
2295
+ from superlocalmemory.cli.setup_wizard import _mark_complete
2296
+
2297
+ # Step 1: write mode-A config (create-if-absent or --force).
2298
+ # Pass slm_data_dir explicitly so env-overridden paths are respected even
2299
+ # when DEFAULT_BASE_DIR was evaluated before the env var was set (e.g. tests).
2300
+ if force or not config_exists:
2301
+ try:
2302
+ cfg = SLMConfig.for_mode(Mode.A, base_dir=slm_data_dir)
2303
+ cfg.save(mode_change=True)
2304
+ except Exception as exc:
2305
+ print(f"[ERROR] slm init --auto: config save failed: {exc}", file=sys.stderr)
2306
+ sys.exit(1)
2307
+
2308
+ # Step 2: mark complete (write .setup-complete sentinel).
2309
+ # Write the sentinel directly using slm_data_dir to avoid the module-level
2310
+ # _SLM_HOME resolved at import time (tests set the env var after import).
2311
+ try:
2312
+ import platform
2313
+ import time as _time
2314
+ sentinel = slm_data_dir / ".setup-complete"
2315
+ slm_data_dir.mkdir(parents=True, exist_ok=True)
2316
+ sentinel.write_text(
2317
+ f"setup_completed={_time.strftime('%Y-%m-%dT%H:%M:%S')}\n"
2318
+ f"python={sys.executable}\n"
2319
+ f"platform={platform.system()}\n"
2320
+ f"version={platform.python_version()}\n"
2321
+ )
2322
+ except Exception:
2323
+ pass # best-effort — sentinel is advisory only
2324
+
2325
+ # Step 3: install hooks if ~/.claude exists.
2326
+ try:
2327
+ claude_dir = Path.home() / ".claude"
2328
+ if claude_dir.exists():
2329
+ from superlocalmemory.hooks.claude_code_hooks import install_hooks
2330
+ install_hooks(include_gate=getattr(args, "gate", False))
2331
+ except Exception:
2332
+ pass # best-effort
2333
+
2334
+ # Step 4: warmup best-effort (don't block if models not present).
2335
+ # Skipped in --auto to keep startup fast for CI; user can run slm warmup.
2336
+
2337
+ print("[OK] slm init --auto: setup complete (mode A, non-interactive)", file=sys.stderr)
2338
+
2339
+
2006
2340
  def cmd_init(args: Namespace) -> None:
2007
2341
  """One-command setup: mode + hooks + IDE connect + warmup."""
2008
2342
  from pathlib import Path
2343
+ from superlocalmemory.cli._lazy_init import slm_home
2009
2344
  from superlocalmemory.core.config import SLMConfig
2010
2345
 
2011
2346
  force = getattr(args, "force", False)
2347
+ auto = getattr(args, "auto", False)
2348
+
2349
+ slm_data_dir = slm_home()
2350
+ config_exists = (slm_data_dir / "config.json").exists()
2012
2351
 
2013
- config_exists = (Path.home() / ".superlocalmemory" / "config.json").exists()
2352
+ # WP-07: --auto branch fully non-interactive, no TTY required (AC6).
2353
+ if auto:
2354
+ os.environ["SLM_NON_INTERACTIVE"] = "1"
2355
+ _cmd_init_auto(args, slm_data_dir, config_exists, force)
2356
+ return
2014
2357
 
2015
2358
  print()
2016
2359
  print("SuperLocalMemory — One-Time Setup")
@@ -2114,41 +2457,7 @@ def cmd_hooks(args: Namespace) -> None:
2114
2457
  if include_gate:
2115
2458
  print(" Gate: ON (enforces session_init — experimental)")
2116
2459
  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
2460
+
2152
2461
  else:
2153
2462
  print(f"Installation failed: {result['errors']}")
2154
2463
  elif action == "remove":
@@ -24,6 +24,8 @@ _os.environ.setdefault('TORCH_DEVICE', 'cpu')
24
24
  import argparse
25
25
  import sys
26
26
 
27
+ from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
28
+
27
29
  _HELP_EPILOG = """\
28
30
  operating modes:
29
31
  Mode A Local Guardian — Zero cloud, zero LLM. All processing stays on
@@ -76,36 +78,61 @@ def main() -> None:
76
78
  handle_hook(sys.argv[2])
77
79
  return
78
80
 
81
+ # WP-07: lazy first-run init — runs after hook/mcp fast-paths so stdout
82
+ # is never polluted on those paths (CRIT-3, MCP JSON-RPC purity).
83
+ # Guarded: any failure must not crash the CLI (AC4).
84
+ _is_mcp_cmd = len(sys.argv) >= 2 and sys.argv[1] == "mcp"
85
+ if not _is_mcp_cmd:
86
+ try:
87
+ from superlocalmemory.cli._lazy_init import _ensure_initialized
88
+ _ensure_initialized()
89
+ except Exception:
90
+ pass
91
+
79
92
  from superlocalmemory.cli.json_output import _get_version
80
93
  _ver = _get_version()
81
94
 
95
+ # v3.6.13 (CRITICAL): the `mcp` stdio transport requires stdout to carry
96
+ # ONLY JSON-RPC. The post-upgrade banner + migration notice below must NEVER
97
+ # run on that path, or the first post-upgrade `slm mcp` launch pollutes
98
+ # stdout and the MCP client (Claude Desktop / Cursor) rejects the stream as
99
+ # "not valid JSON". Skip the whole block for `mcp`; the next human `slm`
100
+ # command emits the banner (to stderr) and writes the version marker.
101
+ _is_mcp_stdio = len(sys.argv) >= 2 and sys.argv[1] == "mcp"
102
+
82
103
  # One-time post-upgrade banner — silent for fresh installs and
83
104
  # same-version runs. Guarded against I/O errors internally.
84
- from superlocalmemory.cli.version_banner import check_and_emit_upgrade_banner
85
- if check_and_emit_upgrade_banner(_ver):
86
- # First post-upgrade invocation: apply the data-dir migration if
87
- # it's safe. When the previous-version daemon is still running
88
- # we defer the next daemon start picks it up.
89
- try:
90
- import logging as _logging
91
- from pathlib import Path as _P
92
- from superlocalmemory.migrations.v3_4_25_to_v3_4_26 import (
93
- migrate_if_safe as _migrate_if_safe,
94
- )
95
- _data = _P(_os.environ.get("SLM_DATA_DIR")
96
- or _P.home() / ".superlocalmemory")
97
- _res = _migrate_if_safe(_data)
98
- if _res.get("status") == "deferred":
105
+ if not _is_mcp_stdio:
106
+ from superlocalmemory.cli.version_banner import check_and_emit_upgrade_banner
107
+ if check_and_emit_upgrade_banner(_ver):
108
+ # First post-upgrade invocation: apply the data-dir migration if
109
+ # it's safe. When the previous-version daemon is still running
110
+ # we defer — the next daemon start picks it up.
111
+ try:
112
+ import logging as _logging
113
+ from pathlib import Path as _P
114
+ from superlocalmemory.migrations.v3_4_25_to_v3_4_26 import (
115
+ migrate_if_safe as _migrate_if_safe,
116
+ )
117
+ # WP-07: route through slm_home() so all 3 env aliases are honoured.
118
+ from superlocalmemory.cli._lazy_init import slm_home as _slm_home
119
+ _data = _slm_home()
120
+ _res = _migrate_if_safe(_data)
121
+ if _res.get("status") == "deferred":
122
+ print(
123
+ " note: data migration deferred — the running SLM "
124
+ "daemon will apply it on its next restart.",
125
+ file=sys.stderr,
126
+ )
127
+ except Exception as _mig_exc:
128
+ import logging as _logging
129
+ _logging.getLogger(__name__).warning(
130
+ "v3.4.26 migrate_if_safe failed: %s — run `slm doctor`", _mig_exc,
131
+ )
99
132
  print(
100
- " note: data migration deferredthe running SLM "
101
- "daemon will apply it on its next restart."
133
+ " note: data migration check failed run `slm doctor` to diagnose.",
134
+ file=sys.stderr,
102
135
  )
103
- except Exception as _mig_exc:
104
- import logging as _logging
105
- _logging.getLogger(__name__).warning(
106
- "v3.4.26 migrate_if_safe failed: %s — run `slm doctor`", _mig_exc,
107
- )
108
- print(" note: data migration check failed — run `slm doctor` to diagnose.")
109
136
 
110
137
  parser = argparse.ArgumentParser(
111
138
  prog="slm",
@@ -127,6 +154,11 @@ def main() -> None:
127
154
  "--gate", action="store_true",
128
155
  help="Enable PreToolUse gate (experimental — blocks tools until session_init)",
129
156
  )
157
+ # WP-07: non-interactive auto setup (pip post-install, CI, scripts).
158
+ init_p.add_argument(
159
+ "--auto", action="store_true",
160
+ help="Non-interactive setup: mode A + hooks (no TTY required, for CI/scripts)",
161
+ )
130
162
 
131
163
  setup_p = sub.add_parser("setup", help="Interactive first-time setup wizard")
132
164
  setup_p.add_argument(
@@ -146,11 +178,32 @@ def main() -> None:
146
178
  )
147
179
 
148
180
  connect_p = sub.add_parser("connect", help="Auto-configure IDE integrations (17+ IDEs)")
149
- connect_p.add_argument("ide", nargs="?", help="Specific IDE to configure")
181
+ connect_p.add_argument("ide", nargs="?", help="Specific IDE to configure (e.g. cursor, codex, continue)")
150
182
  connect_p.add_argument(
151
183
  "--list", action="store_true", help="List all supported IDEs",
152
184
  )
153
185
  connect_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
186
+ # WP-08 CRIT-1: declare missing flags so cmd_connect can read them without getattr fallback
187
+ connect_p.add_argument(
188
+ "--here", action="store_true", default=False,
189
+ help="Write config relative to current working directory (project scope)",
190
+ )
191
+ connect_p.add_argument(
192
+ "--cross-platform", action="store_true", dest="cross_platform", default=False,
193
+ help="Use LLD-05 cross-platform adapter orchestrator",
194
+ )
195
+ connect_p.add_argument(
196
+ "--disable", metavar="ADAPTER", default=None,
197
+ help="Disable a specific cross-platform adapter by name",
198
+ )
199
+ connect_p.add_argument(
200
+ "--profile", metavar="PROFILE", default=None,
201
+ help="Inject SLM_MCP_PROFILE env var into the MCP server block (WP-01)",
202
+ )
203
+ connect_p.add_argument(
204
+ "--dry-run", action="store_true", dest="dry_run", default=False,
205
+ help="Show what would be written without making changes",
206
+ )
154
207
 
155
208
  migrate_p = sub.add_parser("migrate", help="Migrate data from V2 to V3 schema")
156
209
  migrate_p.add_argument(
@@ -187,7 +240,10 @@ def main() -> None:
187
240
  # same search verb the MCP exposes (handlers dict maps both to cmd_recall).
188
241
  recall_p = sub.add_parser("recall", aliases=["search"], help="Semantic search with 4-channel retrieval")
189
242
  recall_p.add_argument("query", help="Search query")
190
- recall_p.add_argument("--limit", type=int, default=10, help="Max results (default 10)")
243
+ recall_p.add_argument(
244
+ "--limit", type=int, default=CANONICAL_RECALL_LIMIT,
245
+ help=f"Max results (default {CANONICAL_RECALL_LIMIT})",
246
+ )
191
247
  recall_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
192
248
  recall_p.add_argument(
193
249
  "--fast", action="store_true",
@@ -28,7 +28,17 @@ from pathlib import Path
28
28
  # Constants
29
29
  # ---------------------------------------------------------------------------
30
30
 
31
- _SLM_HOME = Path(os.environ.get("SL_MEMORY_PATH", Path.home() / ".superlocalmemory"))
31
+ # WP-07: resolve via slm_home() so all 3 env aliases are honoured.
32
+ # Fallback keeps stdlib-only path if the import fails during early bootstrap.
33
+ def _resolve_slm_home() -> Path:
34
+ try:
35
+ from superlocalmemory.cli._lazy_init import slm_home
36
+ return slm_home()
37
+ except Exception:
38
+ return Path(os.environ.get("SL_MEMORY_PATH", "") or Path.home() / ".superlocalmemory")
39
+
40
+
41
+ _SLM_HOME = _resolve_slm_home()
32
42
  _SETUP_MARKER = _SLM_HOME / ".setup-complete"
33
43
  _EMBED_MODEL = "nomic-ai/nomic-embed-text-v1.5"
34
44
  _RERANKER_MODEL = "cross-encoder/ms-marco-MiniLM-L-12-v2"
@@ -703,14 +713,18 @@ def check_first_use(command: str) -> None:
703
713
  if is_setup_complete():
704
714
  return
705
715
 
706
- # Non-interactive: use defaults silently, don't block the command
716
+ # Non-interactive: use defaults silently, don't block the command.
717
+ # CRIT-1: only save config when it does NOT already exist — lazy-init may
718
+ # have already written a valid config.json; overwriting it here would clobber
719
+ # any lazy-init content (e.g. a pre-existing mode-A skeleton).
707
720
  if not is_interactive():
708
- # Just create config with defaults and mark complete
709
721
  try:
710
- from superlocalmemory.core.config import SLMConfig
722
+ from superlocalmemory.core.config import SLMConfig, DEFAULT_BASE_DIR
711
723
  from superlocalmemory.storage.models import Mode
712
- config = SLMConfig.for_mode(Mode.A)
713
- config.save(mode_change=True)
724
+ config_path = DEFAULT_BASE_DIR / "config.json"
725
+ if not config_path.exists():
726
+ cfg = SLMConfig.for_mode(Mode.A)
727
+ cfg.save(mode_change=True)
714
728
  _mark_complete()
715
729
  except Exception:
716
730
  pass
@@ -167,8 +167,12 @@ def check_and_emit_upgrade_banner(current: str) -> bool:
167
167
  if read_marker_version() == current:
168
168
  return False
169
169
 
170
- sys.stdout.write(_banner(prior, current))
171
- sys.stdout.flush()
170
+ # v3.6.13 (CRITICAL): write to STDERR, never stdout. On the `slm mcp`
171
+ # stdio transport, ANY stdout byte that isn't JSON-RPC corrupts the
172
+ # stream and the MCP client (e.g. Claude Desktop) rejects every
173
+ # message as "not valid JSON". The banner is a human notice → stderr.
174
+ sys.stderr.write(_banner(prior, current))
175
+ sys.stderr.flush()
172
176
  write_marker_version(current)
173
177
  return True
174
178
  finally: