superlocalmemory 3.6.13 → 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 (124) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/README.md +187 -741
  3. package/package.json +12 -5
  4. package/plugin/.claude-plugin/plugin.json +20 -0
  5. package/plugin/.mcp.json +12 -0
  6. package/plugin/CLAUDE.md +43 -0
  7. package/plugin/_GENERATED.md +6 -0
  8. package/plugin/agents/slm-memory-advisor.md +43 -0
  9. package/plugin/agents/slm-optimize-advisor.md +38 -0
  10. package/plugin/hooks/hooks.json +14 -0
  11. package/plugin/requirements.txt +1 -0
  12. package/plugin/scripts/ensure-venv.bat +122 -0
  13. package/plugin/scripts/ensure-venv.sh +105 -0
  14. package/plugin/scripts/slm-launch +15 -0
  15. package/plugin/scripts/slm-launch.bat +17 -0
  16. package/plugin/settings.json +16 -0
  17. package/plugin/skills/slm-cache/SKILL.md +140 -0
  18. package/plugin/skills/slm-compress/SKILL.md +143 -0
  19. package/plugin/skills/slm-graph/SKILL.md +300 -0
  20. package/plugin/skills/slm-recall/SKILL.md +196 -0
  21. package/plugin/skills/slm-remember/SKILL.md +182 -0
  22. package/plugin/skills/slm-session/SKILL.md +207 -0
  23. package/plugin/skills/slm-status/SKILL.md +149 -0
  24. package/plugin-src/.mcp.json +12 -0
  25. package/plugin-src/agents/slm-memory-advisor.md +43 -0
  26. package/plugin-src/agents/slm-optimize-advisor.md +38 -0
  27. package/plugin-src/commands/slm-optimize.md +22 -0
  28. package/plugin-src/commands/slm-recall.md +16 -0
  29. package/plugin-src/commands/slm-remember.md +16 -0
  30. package/plugin-src/commands/slm-status.md +15 -0
  31. package/plugin-src/hooks/.gitkeep +0 -0
  32. package/plugin-src/hooks/hooks.json +14 -0
  33. package/plugin-src/manifest.json +25 -0
  34. package/plugin-src/requirements.txt +1 -0
  35. package/plugin-src/rules/AGENTS.md +90 -0
  36. package/plugin-src/rules/CLAUDE.md.fragment +43 -0
  37. package/plugin-src/scripts/ensure-venv.bat +122 -0
  38. package/plugin-src/scripts/ensure-venv.sh +105 -0
  39. package/plugin-src/scripts/slm-launch +15 -0
  40. package/plugin-src/scripts/slm-launch.bat +17 -0
  41. package/plugin-src/settings.json +16 -0
  42. package/plugin-src/skills/slm-cache/SKILL.md +140 -0
  43. package/plugin-src/skills/slm-compress/SKILL.md +143 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +300 -0
  45. package/plugin-src/skills/slm-recall/SKILL.md +196 -0
  46. package/plugin-src/skills/slm-remember/SKILL.md +182 -0
  47. package/plugin-src/skills/slm-session/SKILL.md +207 -0
  48. package/plugin-src/skills/slm-status/SKILL.md +149 -0
  49. package/pyproject.toml +6 -2
  50. package/scripts/__tests__/build-plugin.test.mjs +613 -0
  51. package/scripts/_savings_math.py +270 -0
  52. package/scripts/build-plugin.js +742 -0
  53. package/scripts/dogfood_savings.py +490 -0
  54. package/scripts/install-skills.ps1 +4 -334
  55. package/scripts/install-skills.sh +4 -435
  56. package/scripts/postinstall-interactive.js +0 -27
  57. package/scripts/postinstall.js +21 -2
  58. package/src/superlocalmemory/__init__.py +1 -1
  59. package/src/superlocalmemory/cli/_lazy_init.py +115 -0
  60. package/src/superlocalmemory/cli/commands.py +348 -39
  61. package/src/superlocalmemory/cli/main.py +47 -4
  62. package/src/superlocalmemory/cli/setup_wizard.py +20 -6
  63. package/src/superlocalmemory/core/config.py +79 -9
  64. package/src/superlocalmemory/core/embeddings.py +10 -5
  65. package/src/superlocalmemory/core/engine.py +2 -2
  66. package/src/superlocalmemory/hooks/claude_code_hooks.py +27 -3
  67. package/src/superlocalmemory/hooks/portable_kit.py +506 -0
  68. package/src/superlocalmemory/infra/cloud_backup.py +99 -23
  69. package/src/superlocalmemory/mcp/cli_fallback.py +602 -0
  70. package/src/superlocalmemory/mcp/server.py +75 -4
  71. package/src/superlocalmemory/mcp/tools_code_graph.py +3 -3
  72. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  73. package/src/superlocalmemory/optimize/cache/boundary_store.py +25 -6
  74. package/src/superlocalmemory/optimize/cache/centroid_store.py +27 -4
  75. package/src/superlocalmemory/optimize/cache/manager.py +92 -6
  76. package/src/superlocalmemory/optimize/cache/semantic.py +20 -1
  77. package/src/superlocalmemory/optimize/compress/ccr.py +12 -0
  78. package/src/superlocalmemory/optimize/compress/router.py +46 -13
  79. package/src/superlocalmemory/optimize/config/schema.py +6 -0
  80. package/src/superlocalmemory/optimize/proxy/_helpers.py +111 -8
  81. package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +14 -4
  82. package/src/superlocalmemory/optimize/proxy/gemini_surface.py +23 -6
  83. package/src/superlocalmemory/optimize/proxy/openai_surface.py +10 -4
  84. package/src/superlocalmemory/optimize/proxy/server.py +11 -0
  85. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +246 -0
  86. package/src/superlocalmemory/optimize/storage/db.py +30 -0
  87. package/src/superlocalmemory/server/recall_serializer.py +3 -1
  88. package/src/superlocalmemory/server/unified_daemon.py +24 -6
  89. package/src/superlocalmemory/ui/css/legacy-dashboard.css +18 -0
  90. package/src/superlocalmemory/ui/css/neural-glass.css +5 -0
  91. package/src/superlocalmemory/ui/index.html +2 -2
  92. package/src/superlocalmemory/ui/js/core.js +98 -0
  93. package/src/superlocalmemory/ui/js/dashboard.js +8 -1
  94. package/src/superlocalmemory/ui/js/ide-status.js +16 -3
  95. package/src/superlocalmemory/ui/js/math-health.js +15 -3
  96. package/src/superlocalmemory/ui/js/optimize.js +18 -2
  97. package/src/superlocalmemory/ui/js/trust-dashboard.js +10 -1
  98. package/src/superlocalmemory.egg-info/PKG-INFO +189 -742
  99. package/src/superlocalmemory.egg-info/SOURCES.txt +6 -9
  100. package/src/superlocalmemory.egg-info/requires.txt +1 -0
  101. package/ide/skills/slm-build-graph/SKILL.md +0 -423
  102. package/ide/skills/slm-list-recent/SKILL.md +0 -348
  103. package/ide/skills/slm-recall/SKILL.md +0 -326
  104. package/ide/skills/slm-remember/SKILL.md +0 -194
  105. package/ide/skills/slm-show-patterns/SKILL.md +0 -224
  106. package/ide/skills/slm-status/SKILL.md +0 -363
  107. package/ide/skills/slm-switch-profile/SKILL.md +0 -442
  108. package/skills/slm-build-graph/SKILL.md +0 -423
  109. package/skills/slm-list-recent/SKILL.md +0 -348
  110. package/skills/slm-optimize/README.md +0 -55
  111. package/skills/slm-optimize/SKILL.md +0 -139
  112. package/skills/slm-recall/SKILL.md +0 -343
  113. package/skills/slm-remember/SKILL.md +0 -194
  114. package/skills/slm-show-patterns/SKILL.md +0 -224
  115. package/skills/slm-status/SKILL.md +0 -363
  116. package/skills/slm-switch-profile/SKILL.md +0 -442
  117. package/src/superlocalmemory/cli/doctor_cmd.py +0 -152
  118. package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +0 -423
  119. package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +0 -348
  120. package/src/superlocalmemory/skills/slm-recall/SKILL.md +0 -343
  121. package/src/superlocalmemory/skills/slm-remember/SKILL.md +0 -194
  122. package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +0 -224
  123. package/src/superlocalmemory/skills/slm-status/SKILL.md +0 -363
  124. 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,6 +78,17 @@ 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
 
@@ -101,8 +114,9 @@ def main() -> None:
101
114
  from superlocalmemory.migrations.v3_4_25_to_v3_4_26 import (
102
115
  migrate_if_safe as _migrate_if_safe,
103
116
  )
104
- _data = _P(_os.environ.get("SLM_DATA_DIR")
105
- or _P.home() / ".superlocalmemory")
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()
106
120
  _res = _migrate_if_safe(_data)
107
121
  if _res.get("status") == "deferred":
108
122
  print(
@@ -140,6 +154,11 @@ def main() -> None:
140
154
  "--gate", action="store_true",
141
155
  help="Enable PreToolUse gate (experimental — blocks tools until session_init)",
142
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
+ )
143
162
 
144
163
  setup_p = sub.add_parser("setup", help="Interactive first-time setup wizard")
145
164
  setup_p.add_argument(
@@ -159,11 +178,32 @@ def main() -> None:
159
178
  )
160
179
 
161
180
  connect_p = sub.add_parser("connect", help="Auto-configure IDE integrations (17+ IDEs)")
162
- 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)")
163
182
  connect_p.add_argument(
164
183
  "--list", action="store_true", help="List all supported IDEs",
165
184
  )
166
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
+ )
167
207
 
168
208
  migrate_p = sub.add_parser("migrate", help="Migrate data from V2 to V3 schema")
169
209
  migrate_p.add_argument(
@@ -200,7 +240,10 @@ def main() -> None:
200
240
  # same search verb the MCP exposes (handlers dict maps both to cmd_recall).
201
241
  recall_p = sub.add_parser("recall", aliases=["search"], help="Semantic search with 4-channel retrieval")
202
242
  recall_p.add_argument("query", help="Search query")
203
- 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
+ )
204
247
  recall_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
205
248
  recall_p.add_argument(
206
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