superlocalmemory 3.4.3 → 3.4.4

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.
@@ -0,0 +1,367 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the Elastic License 2.0 - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """OS-level service installer — daemon survives reboots.
6
+
7
+ Cross-platform:
8
+ - macOS: LaunchAgent plist (user-level, no sudo)
9
+ - Linux: systemd user service (no sudo)
10
+ - Windows: Task Scheduler (runs at logon)
11
+
12
+ Usage:
13
+ slm serve install — install OS service
14
+ slm serve uninstall — remove OS service
15
+ slm serve status — show daemon + service status
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import os
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ _SERVICE_NAME = "com.qualixar.superlocalmemory"
30
+ _DISPLAY_NAME = "SuperLocalMemory Daemon"
31
+
32
+
33
+ def get_python_path() -> str:
34
+ """Get the full path to the Python interpreter running SLM."""
35
+ return sys.executable
36
+
37
+
38
+ def get_log_path() -> Path:
39
+ log_dir = Path.home() / ".superlocalmemory" / "logs"
40
+ log_dir.mkdir(parents=True, exist_ok=True)
41
+ return log_dir / "daemon.log"
42
+
43
+
44
+ def get_error_log_path() -> Path:
45
+ log_dir = Path.home() / ".superlocalmemory" / "logs"
46
+ log_dir.mkdir(parents=True, exist_ok=True)
47
+ return log_dir / "daemon-error.log"
48
+
49
+
50
+ # ─── macOS: LaunchAgent ───────────────────────────────────────────────────
51
+
52
+ def _macos_plist_path() -> Path:
53
+ return Path.home() / "Library" / "LaunchAgents" / f"{_SERVICE_NAME}.plist"
54
+
55
+
56
+ def _macos_plist_content() -> str:
57
+ python = get_python_path()
58
+ log = get_log_path()
59
+ err_log = get_error_log_path()
60
+
61
+ return f"""<?xml version="1.0" encoding="UTF-8"?>
62
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
63
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
64
+ <plist version="1.0">
65
+ <dict>
66
+ <key>Label</key>
67
+ <string>{_SERVICE_NAME}</string>
68
+ <key>ProgramArguments</key>
69
+ <array>
70
+ <string>{python}</string>
71
+ <string>-m</string>
72
+ <string>superlocalmemory.server.unified_daemon</string>
73
+ <string>--start</string>
74
+ </array>
75
+ <key>RunAtLoad</key>
76
+ <true/>
77
+ <key>KeepAlive</key>
78
+ <dict>
79
+ <key>SuccessfulExit</key>
80
+ <false/>
81
+ </dict>
82
+ <key>ThrottleInterval</key>
83
+ <integer>30</integer>
84
+ <key>StandardOutPath</key>
85
+ <string>{log}</string>
86
+ <key>StandardErrorPath</key>
87
+ <string>{err_log}</string>
88
+ <key>EnvironmentVariables</key>
89
+ <dict>
90
+ <key>PATH</key>
91
+ <string>/usr/local/bin:/usr/bin:/bin:{Path(python).parent}</string>
92
+ <key>HOME</key>
93
+ <string>{Path.home()}</string>
94
+ </dict>
95
+ </dict>
96
+ </plist>
97
+ """
98
+
99
+
100
+ def install_macos() -> bool:
101
+ plist = _macos_plist_path()
102
+ plist.parent.mkdir(parents=True, exist_ok=True)
103
+ plist.write_text(_macos_plist_content())
104
+ logger.info("Wrote LaunchAgent plist: %s", plist)
105
+
106
+ # Load the service
107
+ try:
108
+ subprocess.run(
109
+ ["launchctl", "unload", str(plist)],
110
+ capture_output=True, timeout=10,
111
+ )
112
+ except Exception:
113
+ pass
114
+
115
+ result = subprocess.run(
116
+ ["launchctl", "load", str(plist)],
117
+ capture_output=True, text=True, timeout=10,
118
+ )
119
+ if result.returncode == 0:
120
+ logger.info("LaunchAgent loaded successfully")
121
+ return True
122
+ else:
123
+ logger.error("launchctl load failed: %s", result.stderr)
124
+ return False
125
+
126
+
127
+ def uninstall_macos() -> bool:
128
+ plist = _macos_plist_path()
129
+ if plist.exists():
130
+ try:
131
+ subprocess.run(
132
+ ["launchctl", "unload", str(plist)],
133
+ capture_output=True, timeout=10,
134
+ )
135
+ except Exception:
136
+ pass
137
+ plist.unlink()
138
+ logger.info("LaunchAgent removed: %s", plist)
139
+ return True
140
+
141
+
142
+ def status_macos() -> dict:
143
+ result = subprocess.run(
144
+ ["launchctl", "list", _SERVICE_NAME],
145
+ capture_output=True, text=True, timeout=10,
146
+ )
147
+ installed = result.returncode == 0
148
+ return {
149
+ "platform": "macOS",
150
+ "service_type": "LaunchAgent",
151
+ "installed": installed,
152
+ "plist_path": str(_macos_plist_path()),
153
+ "details": result.stdout.strip() if installed else "Not installed",
154
+ }
155
+
156
+
157
+ # ─── Linux: systemd user service ──────────────────────────────────────────
158
+
159
+ def _linux_service_path() -> Path:
160
+ return Path.home() / ".config" / "systemd" / "user" / "superlocalmemory.service"
161
+
162
+
163
+ def _linux_service_content() -> str:
164
+ python = get_python_path()
165
+ log = get_log_path()
166
+
167
+ return f"""[Unit]
168
+ Description={_DISPLAY_NAME}
169
+ After=network.target
170
+
171
+ [Service]
172
+ Type=simple
173
+ ExecStart={python} -m superlocalmemory.server.unified_daemon --start
174
+ Restart=on-failure
175
+ RestartSec=30
176
+ StandardOutput=append:{log}
177
+ StandardError=append:{get_error_log_path()}
178
+ Environment=HOME={Path.home()}
179
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin:{Path(python).parent}
180
+
181
+ [Install]
182
+ WantedBy=default.target
183
+ """
184
+
185
+
186
+ def install_linux() -> bool:
187
+ service = _linux_service_path()
188
+ service.parent.mkdir(parents=True, exist_ok=True)
189
+ service.write_text(_linux_service_content())
190
+ logger.info("Wrote systemd user service: %s", service)
191
+
192
+ try:
193
+ subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True, timeout=10)
194
+ subprocess.run(["systemctl", "--user", "enable", "superlocalmemory"], capture_output=True, timeout=10)
195
+ result = subprocess.run(
196
+ ["systemctl", "--user", "start", "superlocalmemory"],
197
+ capture_output=True, text=True, timeout=10,
198
+ )
199
+ if result.returncode == 0:
200
+ logger.info("systemd user service started")
201
+ return True
202
+ else:
203
+ logger.error("systemctl start failed: %s", result.stderr)
204
+ return False
205
+ except FileNotFoundError:
206
+ logger.warning("systemctl not found — systemd not available on this system")
207
+ return False
208
+
209
+
210
+ def uninstall_linux() -> bool:
211
+ try:
212
+ subprocess.run(["systemctl", "--user", "stop", "superlocalmemory"], capture_output=True, timeout=10)
213
+ subprocess.run(["systemctl", "--user", "disable", "superlocalmemory"], capture_output=True, timeout=10)
214
+ except Exception:
215
+ pass
216
+ service = _linux_service_path()
217
+ if service.exists():
218
+ service.unlink()
219
+ logger.info("systemd user service removed: %s", service)
220
+ try:
221
+ subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True, timeout=10)
222
+ except Exception:
223
+ pass
224
+ return True
225
+
226
+
227
+ def status_linux() -> dict:
228
+ try:
229
+ result = subprocess.run(
230
+ ["systemctl", "--user", "is-active", "superlocalmemory"],
231
+ capture_output=True, text=True, timeout=10,
232
+ )
233
+ active = result.stdout.strip() == "active"
234
+ return {
235
+ "platform": "Linux",
236
+ "service_type": "systemd user service",
237
+ "installed": True,
238
+ "active": active,
239
+ "details": result.stdout.strip(),
240
+ }
241
+ except Exception:
242
+ return {"platform": "Linux", "service_type": "systemd", "installed": False}
243
+
244
+
245
+ # ─── Windows: Task Scheduler ─────────────────────────────────────────────
246
+
247
+ _WINDOWS_TASK_NAME = "SuperLocalMemory"
248
+
249
+
250
+ def install_windows() -> bool:
251
+ python = get_python_path()
252
+ log = get_log_path()
253
+
254
+ # Create a VBS wrapper to run Python without console window
255
+ vbs_path = Path.home() / ".superlocalmemory" / "start-daemon.vbs"
256
+ vbs_path.parent.mkdir(parents=True, exist_ok=True)
257
+ vbs_content = (
258
+ f'Set WshShell = CreateObject("WScript.Shell")\n'
259
+ f'WshShell.Run """{python}"" -m superlocalmemory.server.unified_daemon --start", 0, False\n'
260
+ )
261
+ vbs_path.write_text(vbs_content)
262
+
263
+ # Use schtasks to create a logon trigger task
264
+ try:
265
+ # Remove existing task if any
266
+ subprocess.run(
267
+ ["schtasks", "/Delete", "/TN", _WINDOWS_TASK_NAME, "/F"],
268
+ capture_output=True, timeout=10,
269
+ )
270
+ except Exception:
271
+ pass
272
+
273
+ try:
274
+ result = subprocess.run(
275
+ [
276
+ "schtasks", "/Create",
277
+ "/TN", _WINDOWS_TASK_NAME,
278
+ "/TR", f'wscript.exe "{vbs_path}"',
279
+ "/SC", "ONLOGON",
280
+ "/RL", "LIMITED",
281
+ "/F",
282
+ ],
283
+ capture_output=True, text=True, timeout=10,
284
+ )
285
+ if result.returncode == 0:
286
+ logger.info("Windows Task Scheduler task created: %s", _WINDOWS_TASK_NAME)
287
+ return True
288
+ else:
289
+ logger.error("schtasks create failed: %s", result.stderr)
290
+ return False
291
+ except FileNotFoundError:
292
+ logger.warning("schtasks not found — not a standard Windows system")
293
+ return False
294
+
295
+
296
+ def uninstall_windows() -> bool:
297
+ try:
298
+ subprocess.run(
299
+ ["schtasks", "/Delete", "/TN", _WINDOWS_TASK_NAME, "/F"],
300
+ capture_output=True, timeout=10,
301
+ )
302
+ logger.info("Windows Task Scheduler task removed")
303
+ except Exception:
304
+ pass
305
+
306
+ vbs_path = Path.home() / ".superlocalmemory" / "start-daemon.vbs"
307
+ if vbs_path.exists():
308
+ vbs_path.unlink()
309
+
310
+ return True
311
+
312
+
313
+ def status_windows() -> dict:
314
+ try:
315
+ result = subprocess.run(
316
+ ["schtasks", "/Query", "/TN", _WINDOWS_TASK_NAME, "/FO", "LIST"],
317
+ capture_output=True, text=True, timeout=10,
318
+ )
319
+ installed = result.returncode == 0
320
+ return {
321
+ "platform": "Windows",
322
+ "service_type": "Task Scheduler",
323
+ "installed": installed,
324
+ "task_name": _WINDOWS_TASK_NAME,
325
+ "details": result.stdout.strip()[:200] if installed else "Not installed",
326
+ }
327
+ except Exception:
328
+ return {"platform": "Windows", "service_type": "Task Scheduler", "installed": False}
329
+
330
+
331
+ # ─── Cross-platform dispatcher ───────────────────────────────────────────
332
+
333
+ def install_service() -> bool:
334
+ """Install OS-level service for auto-start on login/boot."""
335
+ if sys.platform == "darwin":
336
+ return install_macos()
337
+ elif sys.platform == "win32":
338
+ return install_windows()
339
+ elif sys.platform.startswith("linux"):
340
+ return install_linux()
341
+ else:
342
+ logger.warning("Unsupported platform: %s", sys.platform)
343
+ return False
344
+
345
+
346
+ def uninstall_service() -> bool:
347
+ """Remove OS-level service."""
348
+ if sys.platform == "darwin":
349
+ return uninstall_macos()
350
+ elif sys.platform == "win32":
351
+ return uninstall_windows()
352
+ elif sys.platform.startswith("linux"):
353
+ return uninstall_linux()
354
+ else:
355
+ return False
356
+
357
+
358
+ def service_status() -> dict:
359
+ """Get OS-level service status."""
360
+ if sys.platform == "darwin":
361
+ return status_macos()
362
+ elif sys.platform == "win32":
363
+ return status_windows()
364
+ elif sys.platform.startswith("linux"):
365
+ return status_linux()
366
+ else:
367
+ return {"platform": sys.platform, "installed": False, "details": "Unsupported platform"}
@@ -548,10 +548,23 @@ def run_wizard(auto: bool = False) -> None:
548
548
  print(' slm recall "search query"')
549
549
  print(" slm dashboard → http://localhost:8765")
550
550
  print(" slm adapters enable gmail → start Gmail ingestion")
551
+ print()
552
+ # V3.4.4: Auto-install OS service for daemon persistence (survive reboots)
553
+ try:
554
+ from superlocalmemory.cli.service_installer import install_service
555
+ print(" Installing OS service for auto-start...")
556
+ if install_service():
557
+ print(" ✓ SLM will auto-start on login — zero friction.")
558
+ else:
559
+ print(" ⚠ OS service not installed (run: slm serve install)")
560
+ except Exception:
561
+ print(" ⚠ Could not install OS service (run: slm serve install)")
562
+
551
563
  print()
552
564
  print(" Need help?")
553
565
  print(" slm doctor — diagnose issues")
554
566
  print(" slm --help — all commands")
567
+ print(" slm serve install — install auto-start service")
555
568
  print(" https://github.com/qualixar/superlocalmemory")
556
569
  print()
557
570
 
@@ -65,7 +65,7 @@ def reset_engine():
65
65
 
66
66
  import os as _os_reg
67
67
 
68
- _ESSENTIAL_TOOLS: frozenset[str] = frozenset({
68
+ _ESSENTIAL_TOOLS: set[str] = {
69
69
  # Core memory operations (8)
70
70
  "remember", "recall", "search", "fetch",
71
71
  "list_recent", "delete_memory", "update_memory", "get_status",
@@ -76,7 +76,25 @@ _ESSENTIAL_TOOLS: frozenset[str] = frozenset({
76
76
  # Infinite memory + learning (4)
77
77
  "consolidate_cognitive", "get_soft_prompts",
78
78
  "set_mode", "report_outcome",
79
- })
79
+ }
80
+
81
+ # v3.4.4: Mesh tools — enabled if mesh_enabled in config or SLM_MCP_MESH_TOOLS=1
82
+ _mesh_tools_enabled = _os_reg.environ.get("SLM_MCP_MESH_TOOLS", "").lower() in ("1", "true")
83
+ if not _mesh_tools_enabled:
84
+ try:
85
+ from superlocalmemory.core.config import SLMConfig
86
+ _cfg = SLMConfig.load()
87
+ _mesh_tools_enabled = getattr(_cfg, "mesh_enabled", True) # default True in v3.4.3+
88
+ except Exception:
89
+ _mesh_tools_enabled = True # Safe default — mesh broker is always in daemon
90
+
91
+ if _mesh_tools_enabled:
92
+ _ESSENTIAL_TOOLS.update({
93
+ "mesh_summary", "mesh_peers", "mesh_send", "mesh_inbox",
94
+ "mesh_state", "mesh_lock", "mesh_events", "mesh_status",
95
+ })
96
+
97
+ _ESSENTIAL_TOOLS = frozenset(_ESSENTIAL_TOOLS)
80
98
 
81
99
  _all_tools = _os_reg.environ.get("SLM_MCP_ALL_TOOLS") == "1"
82
100
 
@@ -115,6 +133,7 @@ from superlocalmemory.mcp.tools_active import register_active_tools
115
133
  from superlocalmemory.mcp.tools_v33 import register_v33_tools
116
134
  from superlocalmemory.mcp.resources import register_resources
117
135
  from superlocalmemory.mcp.tools_code_graph import register_code_graph_tools
136
+ from superlocalmemory.mcp.tools_mesh import register_mesh_tools
118
137
 
119
138
  register_core_tools(_target, get_engine)
120
139
  register_v28_tools(_target, get_engine)
@@ -123,6 +142,7 @@ register_active_tools(_target, get_engine)
123
142
  register_v33_tools(_target, get_engine)
124
143
  register_resources(server, get_engine) # Resources always registered (not tools)
125
144
  register_code_graph_tools(_target, get_engine) # CodeGraph: filtered like other tools (SLM_MCP_ALL_TOOLS=1 to show all)
145
+ register_mesh_tools(_target, get_engine) # v3.4.4: Mesh P2P tools — ships with SLM, no separate slm-mesh needed
126
146
 
127
147
 
128
148
  # V3.3.21: Eager engine warmup — start initializing BEFORE first tool call.
@@ -132,7 +152,7 @@ register_code_graph_tools(_target, get_engine) # CodeGraph: filtered like other
132
152
  # the first tool call arrives (1-2s later), the engine is already warm.
133
153
  # This applies to ALL IDEs: Claude Code, Cursor, Antigravity, Gemini CLI, etc.
134
154
  def _eager_warmup() -> None:
135
- """Pre-warm engine in background thread."""
155
+ """Pre-warm engine + ensure daemon is running (background thread)."""
136
156
  import logging
137
157
  _logger = logging.getLogger(__name__)
138
158
  try:
@@ -141,6 +161,15 @@ def _eager_warmup() -> None:
141
161
  except Exception as exc:
142
162
  _logger.debug("MCP engine pre-warmup failed (non-fatal): %s", exc)
143
163
 
164
+ # V3.4.4: Also ensure daemon is running for dashboard/mesh/health features.
165
+ # This runs in background — doesn't block MCP tool registration.
166
+ try:
167
+ from superlocalmemory.cli.daemon import ensure_daemon
168
+ if ensure_daemon():
169
+ _logger.info("Daemon auto-started by MCP server")
170
+ except Exception as exc:
171
+ _logger.debug("Daemon auto-start failed (non-fatal): %s", exc)
172
+
144
173
  import threading
145
174
  _warmup_thread = threading.Thread(target=_eager_warmup, daemon=True, name="mcp-warmup")
146
175
  _warmup_thread.start()