superlocalmemory 3.4.1 → 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.
Files changed (47) hide show
  1. package/README.md +9 -12
  2. package/package.json +1 -1
  3. package/pyproject.toml +11 -2
  4. package/scripts/postinstall.js +26 -7
  5. package/src/superlocalmemory/cli/commands.py +71 -60
  6. package/src/superlocalmemory/cli/daemon.py +184 -64
  7. package/src/superlocalmemory/cli/main.py +25 -2
  8. package/src/superlocalmemory/cli/service_installer.py +367 -0
  9. package/src/superlocalmemory/cli/setup_wizard.py +150 -9
  10. package/src/superlocalmemory/core/config.py +28 -0
  11. package/src/superlocalmemory/core/consolidation_engine.py +38 -1
  12. package/src/superlocalmemory/core/engine.py +9 -0
  13. package/src/superlocalmemory/core/health_monitor.py +313 -0
  14. package/src/superlocalmemory/core/reranker_worker.py +19 -5
  15. package/src/superlocalmemory/ingestion/__init__.py +13 -0
  16. package/src/superlocalmemory/ingestion/adapter_manager.py +234 -0
  17. package/src/superlocalmemory/ingestion/base_adapter.py +177 -0
  18. package/src/superlocalmemory/ingestion/calendar_adapter.py +340 -0
  19. package/src/superlocalmemory/ingestion/credentials.py +118 -0
  20. package/src/superlocalmemory/ingestion/gmail_adapter.py +369 -0
  21. package/src/superlocalmemory/ingestion/parsers.py +100 -0
  22. package/src/superlocalmemory/ingestion/transcript_adapter.py +156 -0
  23. package/src/superlocalmemory/learning/consolidation_worker.py +47 -1
  24. package/src/superlocalmemory/learning/entity_compiler.py +377 -0
  25. package/src/superlocalmemory/mcp/server.py +32 -3
  26. package/src/superlocalmemory/mcp/tools_mesh.py +249 -0
  27. package/src/superlocalmemory/mesh/__init__.py +12 -0
  28. package/src/superlocalmemory/mesh/broker.py +344 -0
  29. package/src/superlocalmemory/retrieval/entity_channel.py +12 -6
  30. package/src/superlocalmemory/server/api.py +6 -7
  31. package/src/superlocalmemory/server/routes/adapters.py +63 -0
  32. package/src/superlocalmemory/server/routes/entity.py +151 -0
  33. package/src/superlocalmemory/server/routes/ingest.py +110 -0
  34. package/src/superlocalmemory/server/routes/mesh.py +186 -0
  35. package/src/superlocalmemory/server/unified_daemon.py +693 -0
  36. package/src/superlocalmemory/storage/schema_v343.py +229 -0
  37. package/src/superlocalmemory/ui/css/neural-glass.css +1588 -0
  38. package/src/superlocalmemory/ui/index.html +134 -4
  39. package/src/superlocalmemory/ui/js/memory-chat.js +28 -1
  40. package/src/superlocalmemory/ui/js/ng-entities.js +272 -0
  41. package/src/superlocalmemory/ui/js/ng-health.js +208 -0
  42. package/src/superlocalmemory/ui/js/ng-ingestion.js +203 -0
  43. package/src/superlocalmemory/ui/js/ng-mesh.js +311 -0
  44. package/src/superlocalmemory/ui/js/ng-shell.js +471 -0
  45. package/src/superlocalmemory.egg-info/PKG-INFO +18 -14
  46. package/src/superlocalmemory.egg-info/SOURCES.txt +26 -0
  47. package/src/superlocalmemory.egg-info/requires.txt +9 -1
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
  <img src="https://superlocalmemory.com/assets/logo-mark.png" alt="SuperLocalMemory" width="200"/>
3
3
  </p>
4
4
 
5
- <h1 align="center">SuperLocalMemory V3.3</h1>
5
+ <h1 align="center">SuperLocalMemory V3.4</h1>
6
6
  <p align="center"><strong>Every other AI forgets. Yours won't.</strong><br/><em>Infinite memory for Claude Code, Cursor, Windsurf & 17+ AI tools.</em></p>
7
- <p align="center"><code>v3.3.26</code> — Install once. Every session remembers the last. Automatically.</p>
7
+ <p align="center"><code>v3.4.4 "Neural Glass"</code> — Install once. Every session remembers the last. Automatically.</p>
8
8
  <p align="center"><strong>Backed by 3 peer-reviewed research papers</strong> · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>
9
9
 
10
10
  <p align="center">
@@ -340,20 +340,17 @@ slm dashboard # Opens at http://localhost:8765
340
340
  ```
341
341
 
342
342
  <details open>
343
- <summary><strong>Dashboard Screenshots</strong> (click to collapse)</summary>
344
- <p align="center"><img src="docs/screenshots/01-dashboard-main.png" alt="Dashboard Overview — 3,100+ memories, 430K connections" width="600"/></p>
343
+ <summary><strong>Dashboard Demo — v3.4.4 "Neural Glass"</strong> (click to collapse)</summary>
345
344
  <p align="center">
346
- <img src="docs/screenshots/02-knowledge-graph.png" alt="Knowledge Graph — Sigma.js WebGL with community detection, chat, quick actions, timeline" width="290"/>
347
- <img src="docs/screenshots/06-graph-communities.png" alt="Graph Communities Louvain clustering with colored nodes" width="290"/>
348
- </p>
349
- <p align="center">
350
- <img src="docs/screenshots/03-patterns-learning.png" alt="Patterns — 50 learned behavioral patterns with confidence bars" width="190"/>
351
- <img src="docs/screenshots/04-learning-dashboard.png" alt="Learning — 722 signals, ML Model phase, tech preferences" width="190"/>
352
- <img src="docs/screenshots/05-behavioral-analysis.png" alt="Behavioral — pattern analysis with confidence distribution" width="190"/>
345
+ <a href="https://github.com/qualixar/superlocalmemory/issues/15">
346
+ <video src="https://github.com/user-attachments/assets/c3b54a1d-f62a-4ea7-bba7-900435e7b3ab" width="800" autoplay loop muted playsinline>
347
+ Your browser does not support the video tag. <a href="https://github.com/qualixar/superlocalmemory/issues/15">Watch the demo</a>.
348
+ </video>
349
+ </a>
353
350
  </p>
354
351
  </details>
355
352
 
356
- **v3.4.1 Visual Intelligence:** Sigma.js WebGL knowledge graph with community detection (Louvain/Leiden), 5 quick insight actions, D3 memory timeline, graph-enhanced retrieval (PageRank bias + community boost + contradiction suppression), and 56 auto-mined behavioral patterns. 23+ tabs. Runs locally — no data leaves your machine.
353
+ **v3.4.4 "Neural Glass":** 21-tab sidebar dashboard with light + dark theme. Knowledge Graph (Sigma.js WebGL, community detection), Health Monitor, Entity Explorer (1,300+ entities), Mesh Peers (P2P agent communication), Ingestion Status (Gmail/Calendar/Transcript management), Privacy blur mode. Always-on daemon with auto-start. 8 mesh MCP tools built-in. Cross-platform: macOS + Windows + Linux. All data stays local.
357
354
 
358
355
  ---
359
356
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.4.1",
3
+ "version": "3.4.4",
4
4
  "description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
5
5
  "keywords": [
6
6
  "ai-memory",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.4.1"
3
+ version = "3.4.4"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0-or-later"}
@@ -49,6 +49,9 @@ dependencies = [
49
49
  "tree-sitter-language-pack>=0.3,<2",
50
50
  "rustworkx>=0.15,<1",
51
51
  "watchdog>=4.0,<6",
52
+ # V3.4.3: Unified Brain
53
+ "psutil>=5.9.0",
54
+ "structlog>=24.0.0,<27.0.0",
52
55
  ]
53
56
 
54
57
  [project.optional-dependencies]
@@ -72,8 +75,14 @@ performance = [
72
75
  "diskcache>=5.6.0",
73
76
  "orjson>=3.9.0",
74
77
  ]
78
+ ingestion = [
79
+ "keyring>=25.0.0",
80
+ "google-auth-oauthlib>=1.2.0",
81
+ "google-api-python-client>=2.100.0",
82
+ "icalendar>=6.0.0",
83
+ ]
75
84
  full = [
76
- "superlocalmemory[search,ui,learning,performance]",
85
+ "superlocalmemory[search,ui,learning,performance,ingestion]",
77
86
  ]
78
87
  dev = [
79
88
  "pytest>=8.0",
@@ -15,7 +15,7 @@ const os = require('os');
15
15
  const fs = require('fs');
16
16
 
17
17
  console.log('\n════════════════════════════════════════════════════════════');
18
- console.log(' SuperLocalMemory V3 — Post-Installation');
18
+ console.log(' SuperLocalMemory V3 — The Unified Brain');
19
19
  console.log(' by Varun Pratap Bhardwaj / Qualixar');
20
20
  console.log(' https://github.com/qualixar/superlocalmemory');
21
21
  console.log('════════════════════════════════════════════════════════════\n');
@@ -164,6 +164,17 @@ if (pipInstall(perfDeps, 'performance')) {
164
164
  console.log('⚠ Performance deps skipped (system works fine without them).');
165
165
  }
166
166
 
167
+ // V3.4.3: Unified Brain dependencies (health monitor, structured logging, file watching)
168
+ const brainDeps = ['psutil>=5.9.0', 'structlog>=24.0.0', 'watchdog>=4.0.0'];
169
+ console.log('\nInstalling Unified Brain dependencies (health monitor, file watcher)...');
170
+ if (pipInstall(brainDeps, 'brain')) {
171
+ console.log('✓ Unified Brain deps installed (psutil + structlog + watchdog)');
172
+ console.log(' Health monitoring, structured logging, and file watching enabled');
173
+ } else {
174
+ console.log('⚠ Unified Brain deps partially installed (health monitoring may be limited).');
175
+ console.log(' Run manually: pip install psutil structlog watchdog');
176
+ }
177
+
167
178
  // --- Step 3b: Install the superlocalmemory package itself ---
168
179
  // This ensures `python -m superlocalmemory.cli.main` always resolves the
169
180
  // correct version, even when invoked outside the Node.js wrapper (e.g.,
@@ -281,13 +292,21 @@ if (setupResult.status === 0) {
281
292
 
282
293
  // --- Done ---
283
294
  console.log('\n════════════════════════════════════════════════════════════');
284
- console.log(' ✓ SuperLocalMemory V3 installed!');
295
+ console.log(' ✓ SuperLocalMemory V3 — The Unified Brain installed!');
285
296
  console.log('');
286
297
  console.log(' Quick start:');
287
- console.log(' slm remember "..." # Store a memory');
288
- console.log(' slm recall "..." # Search memories');
289
- console.log(' slm dashboard # Open web dashboard');
290
- console.log(' slm setup # Re-run setup wizard');
298
+ console.log(' slm remember "..." # Store a memory');
299
+ console.log(' slm recall "..." # Search memories');
300
+ console.log(' slm dashboard # Open web dashboard');
301
+ console.log(' slm serve # Start 24/7 daemon');
302
+ console.log(' slm adapters enable gmail # Enable Gmail ingestion');
303
+ console.log(' slm setup # Re-run 9-step wizard');
304
+ console.log('');
305
+ console.log(' New in v3.4.3:');
306
+ console.log(' • Unified daemon (one process, 24/7, < 700MB)');
307
+ console.log(' • SLM Mesh (agent-to-agent P2P built in)');
308
+ console.log(' • Entity compilation (auto knowledge summaries)');
309
+ console.log(' • Ingestion adapters (Gmail, Calendar, Transcripts)');
291
310
  console.log('');
292
- console.log(' Docs: https://github.com/qualixar/superlocalmemory/wiki');
311
+ console.log(' Docs: https://github.com/qualixar/superlocalmemory');
293
312
  console.log('════════════════════════════════════════════════════════════\n');
@@ -58,6 +58,8 @@ def dispatch(args: Namespace) -> None:
58
58
  "reap": cmd_reap,
59
59
  # V3.3.21 daemon
60
60
  "serve": cmd_serve,
61
+ # V3.4.3 ingestion adapters
62
+ "adapters": cmd_adapters,
61
63
  }
62
64
  handler = handlers.get(args.command)
63
65
  if handler:
@@ -94,6 +96,35 @@ def cmd_serve(args: Namespace) -> None:
94
96
  print("Daemon: RUNNING (could not get status)")
95
97
  else:
96
98
  print("Daemon: NOT RUNNING")
99
+ # Also show OS service status
100
+ try:
101
+ from superlocalmemory.cli.service_installer import service_status
102
+ svc = service_status()
103
+ installed = svc.get("installed", False)
104
+ print(f"OS Service: {'INSTALLED' if installed else 'NOT INSTALLED'} "
105
+ f"({svc.get('service_type', svc.get('platform', '?'))})")
106
+ except Exception:
107
+ pass
108
+ return
109
+
110
+ if action == 'install':
111
+ # Install OS-level service for auto-start on boot/login
112
+ from superlocalmemory.cli.service_installer import install_service
113
+ print("Installing SLM as OS service (auto-start on login)...")
114
+ if install_service():
115
+ print("Service installed \u2713 — SLM will auto-start on login.")
116
+ print(" slm serve status — check service status")
117
+ print(" slm serve uninstall — remove auto-start")
118
+ else:
119
+ print("Failed to install service. Check logs.")
120
+ return
121
+
122
+ if action == 'uninstall':
123
+ from superlocalmemory.cli.service_installer import uninstall_service
124
+ if uninstall_service():
125
+ print("OS service removed \u2713 — SLM will no longer auto-start.")
126
+ else:
127
+ print("Failed to remove service.")
97
128
  return
98
129
 
99
130
  # Default: start
@@ -110,6 +141,26 @@ def cmd_serve(args: Namespace) -> None:
110
141
  print("Failed to start daemon. Check ~/.superlocalmemory/logs/daemon.log")
111
142
 
112
143
 
144
+ # -- Ingestion Adapters (V3.4.3) ------------------------------------------
145
+
146
+
147
+ def cmd_adapters(args: Namespace) -> None:
148
+ """Manage ingestion adapters (Gmail, Calendar, Transcript).
149
+
150
+ Usage:
151
+ slm adapters list — show all adapters
152
+ slm adapters enable <name> — enable an adapter
153
+ slm adapters disable <name> — disable and stop
154
+ slm adapters start <name> — start running
155
+ slm adapters stop <name> — stop running
156
+ slm adapters status — detailed status
157
+ """
158
+ from superlocalmemory.ingestion.adapter_manager import handle_adapters_cli
159
+ # args.rest contains everything after "adapters"
160
+ rest = getattr(args, 'rest', []) or []
161
+ handle_adapters_cli(rest)
162
+
163
+
113
164
  # -- Setup & Config (no --json — interactive commands) ---------------------
114
165
 
115
166
 
@@ -1159,72 +1210,32 @@ def _warmup_diagnose() -> None:
1159
1210
 
1160
1211
 
1161
1212
  def cmd_dashboard(args: Namespace) -> None:
1162
- """Launch the web dashboard."""
1163
- try:
1164
- import uvicorn
1165
- except ImportError:
1166
- print("Dashboard requires additional deps. Run: slm doctor")
1167
- print("Or install manually: pip install 'fastapi[all]' uvicorn")
1168
- sys.exit(1)
1169
-
1170
- import os
1171
- import signal
1172
- import socket
1173
-
1174
- port = getattr(args, "port", 8765)
1175
-
1176
- def _kill_existing_on_port(target_port: int) -> None:
1177
- """Kill any existing SLM dashboard on the target port.
1213
+ """Open the web dashboard in the browser.
1178
1214
 
1179
- V3.3.2: ONE port, no auto-increment. If port is busy with
1180
- another SLM instance, kill it. If busy with a non-SLM process,
1181
- warn and exit never silently shift to a different port.
1182
- """
1183
- if sys.platform == "win32":
1184
- return # Windows: user must close manually
1185
- try:
1186
- import subprocess
1187
- result = subprocess.run(
1188
- ["lsof", "-ti", f":{target_port}"],
1189
- capture_output=True, text=True, timeout=5,
1190
- )
1191
- if result.returncode == 0 and result.stdout.strip():
1192
- pids = result.stdout.strip().split("\n")
1193
- for pid_str in pids:
1194
- pid = int(pid_str.strip())
1195
- if pid == os.getpid():
1196
- continue
1197
- # Check if it's an SLM/Python process
1198
- ps_result = subprocess.run(
1199
- ["ps", "-p", str(pid), "-o", "command="],
1200
- capture_output=True, text=True, timeout=5,
1201
- )
1202
- cmd = ps_result.stdout.strip().lower()
1203
- if "superlocalmemory" in cmd or "slm" in cmd or "uvicorn" in cmd:
1204
- os.kill(pid, signal.SIGTERM)
1205
- print(f" Stopped previous dashboard (PID {pid})")
1206
- import time
1207
- time.sleep(1)
1208
- except Exception:
1209
- pass # Best-effort
1210
-
1211
- _kill_existing_on_port(port)
1215
+ v3.4.3: Dashboard is now served by the unified daemon. This command
1216
+ ensures the daemon is running and opens the browser. It does NOT
1217
+ start a separate server (saves ~500MB RAM from duplicate engine).
1218
+ """
1219
+ from superlocalmemory.cli.daemon import ensure_daemon, _get_port
1212
1220
 
1213
- # Brief wait for port to fully release after killing old process
1214
- import time
1215
- time.sleep(1)
1221
+ port = getattr(args, "port", None) or _get_port()
1216
1222
 
1217
- print("=" * 60)
1218
1223
  print(" SuperLocalMemory V3 — Web Dashboard")
1219
- print("=" * 60)
1220
- print(f" Dashboard: http://localhost:{port}")
1221
- print(f" API Docs: http://localhost:{port}/api/docs")
1222
- print(" Press Ctrl+C to stop\n")
1224
+ print(f" Starting daemon if needed...")
1225
+
1226
+ if not ensure_daemon():
1227
+ print(" Could not start daemon. Run `slm doctor` to diagnose.")
1228
+ sys.exit(1)
1223
1229
 
1224
- from superlocalmemory.server.ui import create_app
1230
+ url = f"http://localhost:{port}"
1231
+ print(f" ✓ Daemon running")
1232
+ print(f" Dashboard: {url}")
1233
+ print(f" API Docs: {url}/docs")
1225
1234
 
1226
- app = create_app()
1227
- uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")
1235
+ # Open browser
1236
+ import webbrowser
1237
+ webbrowser.open(url)
1238
+ print("\n Dashboard opened in browser. Daemon continues running in background.")
1228
1239
 
1229
1240
 
1230
1241
  # -- Profiles (supports --json) -------------------------------------------
@@ -2,26 +2,18 @@
2
2
  # Licensed under the Elastic License 2.0 - see LICENSE file
3
3
  # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
4
 
5
- """SLM Daemon — keeps engine warm for instant CLI/MCP response.
5
+ """SLM Daemon — client functions for communicating with the unified daemon.
6
6
 
7
- Problem: CLI cold start is 23s (embedding worker spawn + model load).
8
- Solution: Background daemon keeps MemoryEngine warm. CLI commands route
9
- requests through the daemon via localhost HTTP (~10ms overhead).
7
+ The unified daemon (server/unified_daemon.py) runs as a single FastAPI/uvicorn
8
+ process on port 8765, with port 8767 as a backward-compat TCP redirect.
10
9
 
11
- Architecture:
12
- slm serve → starts daemon (engine init, workers warm, ~600MB RAM)
13
- slm remember X → HTTP POST to daemon instant (no cold start)
14
- slm recall X → HTTP GET from daemon → instant
15
- slm serve stop → graceful shutdown, workers killed, RAM freed
10
+ This module contains CLIENT functions used by CLI commands:
11
+ - is_daemon_running(): check if daemon is alive
12
+ - ensure_daemon(): start daemon if not running
13
+ - stop_daemon(): gracefully stop the daemon
14
+ - daemon_request(): send HTTP request to daemon
16
15
 
17
- Auto-start: if daemon not running on CLI use, starts it automatically.
18
- Auto-shutdown: after 30 min idle (configurable via SLM_DAEMON_IDLE_TIMEOUT).
19
-
20
- Memory safety:
21
- - RSS watchdog on embedding worker (2.5GB cap)
22
- - Worker recycling every 5000 requests
23
- - Parent watchdog kills workers if daemon dies
24
- - SQLite WAL mode for concurrent access
16
+ The actual daemon server code is in server/unified_daemon.py.
25
17
 
26
18
  Part of Qualixar | Author: Varun Pratap Bhardwaj
27
19
  License: Elastic-2.0
@@ -42,8 +34,9 @@ from threading import Thread
42
34
 
43
35
  logger = logging.getLogger(__name__)
44
36
 
45
- _DEFAULT_PORT = 8767
46
- _DEFAULT_IDLE_TIMEOUT = 1800 # 30 min
37
+ _DEFAULT_PORT = 8765 # v3.4.3: unified daemon on 8765 (was 8767)
38
+ _LEGACY_PORT = 8767 # backward-compat redirect
39
+ _DEFAULT_IDLE_TIMEOUT = 0 # v3.4.3: 24/7 default (was 1800)
47
40
  _PID_FILE = Path.home() / ".superlocalmemory" / "daemon.pid"
48
41
  _PORT_FILE = Path.home() / ".superlocalmemory" / "daemon.port"
49
42
 
@@ -52,27 +45,69 @@ _PORT_FILE = Path.home() / ".superlocalmemory" / "daemon.port"
52
45
  # Client: check if daemon running + send requests
53
46
  # ---------------------------------------------------------------------------
54
47
 
55
- def is_daemon_running() -> bool:
56
- """Check if daemon is alive via PID file + HTTP health check."""
57
- if not _PID_FILE.exists():
58
- return False
48
+ def _is_pid_alive(pid: int) -> bool:
49
+ """Cross-platform check if a process with given PID exists."""
59
50
  try:
60
- pid = int(_PID_FILE.read_text().strip())
61
- os.kill(pid, 0) # Check if process exists
62
- except (ValueError, ProcessLookupError, PermissionError):
63
- _PID_FILE.unlink(missing_ok=True)
64
- return False
51
+ import psutil
52
+ return psutil.pid_exists(pid)
53
+ except ImportError:
54
+ try:
55
+ os.kill(pid, 0)
56
+ return True
57
+ except (ProcessLookupError, PermissionError):
58
+ return False
65
59
 
66
- # PID exists — verify HTTP health
67
- port = _get_port()
68
- try:
69
- import urllib.request
70
- resp = urllib.request.urlopen(
71
- f"http://127.0.0.1:{port}/health", timeout=2,
72
- )
73
- return resp.status == 200
74
- except Exception:
75
- return False
60
+
61
+ def is_daemon_running() -> bool:
62
+ """Check if daemon is alive via PID file + HTTP health check.
63
+
64
+ v3.4.4 FIX: If PID is alive, returns True EVEN IF health check fails.
65
+ This prevents starting duplicate daemons when the existing one is
66
+ warming up (Ollama processing, model download, embedding init).
67
+
68
+ Priority:
69
+ 1. PID file exists AND process alive → True (daemon warming up or ready)
70
+ 2. No PID file → try health check on known ports (MCP/hook started daemon)
71
+ 3. PID file stale (process dead) → clean up, return False
72
+ """
73
+ if _PID_FILE.exists():
74
+ try:
75
+ pid = int(_PID_FILE.read_text().strip())
76
+ if _is_pid_alive(pid):
77
+ # PID alive = daemon exists. Don't check health — it might be warming up.
78
+ # This is the critical fix: NEVER start a second daemon if PID is alive.
79
+ return True
80
+ else:
81
+ # Process died — clean up stale PID file
82
+ _PID_FILE.unlink(missing_ok=True)
83
+ _PORT_FILE.unlink(missing_ok=True)
84
+ except (ValueError, OSError):
85
+ _PID_FILE.unlink(missing_ok=True)
86
+
87
+ # No PID file — maybe daemon was started by MCP/hook without PID file.
88
+ # Try health check on known ports as last resort.
89
+ for try_port in (_DEFAULT_PORT, _LEGACY_PORT):
90
+ try:
91
+ import urllib.request
92
+ resp = urllib.request.urlopen(
93
+ f"http://127.0.0.1:{try_port}/health", timeout=2,
94
+ )
95
+ if resp.status == 200:
96
+ # Daemon running without PID file — write one for future checks
97
+ try:
98
+ import json as _json
99
+ data = _json.loads(resp.read().decode())
100
+ pid = data.get("pid")
101
+ if pid:
102
+ _PID_FILE.parent.mkdir(parents=True, exist_ok=True)
103
+ _PID_FILE.write_text(str(pid))
104
+ _PORT_FILE.write_text(str(try_port))
105
+ except Exception:
106
+ pass
107
+ return True
108
+ except Exception:
109
+ continue
110
+ return False
76
111
 
77
112
 
78
113
  def _get_port() -> int:
@@ -99,47 +134,132 @@ def daemon_request(method: str, path: str, body: dict | None = None) -> dict | N
99
134
  return None
100
135
 
101
136
 
137
+ _LOCK_FILE = Path.home() / ".superlocalmemory" / "daemon.lock"
138
+
139
+
102
140
  def ensure_daemon() -> bool:
103
- """Start daemon if not running. Returns True if daemon is ready."""
141
+ """Start daemon if not running. Returns True if daemon is ready.
142
+
143
+ v3.4.4 BULLETPROOF:
144
+ 1. If PID alive → return True immediately (even if warming up)
145
+ 2. File lock prevents two callers from starting concurrent daemons
146
+ 3. After starting, waits for PID file (not health check) — fast detection
147
+ 4. Cross-platform: macOS + Windows + Linux
148
+ """
104
149
  if is_daemon_running():
105
150
  return True
106
151
 
107
- # Start daemon in background
108
- import subprocess
109
- cmd = [sys.executable, "-m", "superlocalmemory.cli.daemon", "--start"]
110
- log_dir = Path.home() / ".superlocalmemory" / "logs"
111
- log_dir.mkdir(parents=True, exist_ok=True)
112
- log_file = log_dir / "daemon.log"
113
-
114
- with open(log_file, "a") as lf:
115
- subprocess.Popen(
116
- cmd, stdout=lf, stderr=lf,
117
- start_new_session=True,
118
- )
119
-
120
- # Wait for daemon to become ready (max 30s for cold start)
121
- for _ in range(60):
122
- time.sleep(0.5)
152
+ # File lock prevent concurrent starts from multiple CLI/MCP calls
153
+ lock_fd = None
154
+ try:
155
+ _LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
156
+ lock_fd = open(_LOCK_FILE, "w")
157
+
158
+ # Cross-platform file locking
159
+ if sys.platform == "win32":
160
+ import msvcrt
161
+ try:
162
+ msvcrt.locking(lock_fd.fileno(), msvcrt.LK_NBLCK, 1)
163
+ except (IOError, OSError):
164
+ # Another process is starting the daemon — just wait for it
165
+ lock_fd.close()
166
+ return _wait_for_daemon(timeout=60)
167
+ else:
168
+ import fcntl
169
+ try:
170
+ fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
171
+ except (IOError, OSError):
172
+ lock_fd.close()
173
+ return _wait_for_daemon(timeout=60)
174
+
175
+ # Re-check after acquiring lock (another process may have started it)
123
176
  if is_daemon_running():
124
177
  return True
125
178
 
179
+ # Start unified daemon in background
180
+ import subprocess
181
+ cmd = [sys.executable, "-m", "superlocalmemory.server.unified_daemon", "--start"]
182
+ log_dir = Path.home() / ".superlocalmemory" / "logs"
183
+ log_dir.mkdir(parents=True, exist_ok=True)
184
+ log_file = log_dir / "daemon.log"
185
+
186
+ kwargs: dict = {}
187
+ if sys.platform == "win32":
188
+ kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
189
+ else:
190
+ kwargs["start_new_session"] = True
191
+
192
+ with open(log_file, "a") as lf:
193
+ proc = subprocess.Popen(cmd, stdout=lf, stderr=lf, **kwargs)
194
+
195
+ # Write PID immediately so other callers see it during warmup
196
+ _PID_FILE.write_text(str(proc.pid))
197
+ _PORT_FILE.write_text(str(_DEFAULT_PORT))
198
+
199
+ return _wait_for_daemon(timeout=60)
200
+
201
+ except Exception as exc:
202
+ logger.debug("ensure_daemon error: %s", exc)
203
+ return False
204
+ finally:
205
+ if lock_fd:
206
+ try:
207
+ lock_fd.close()
208
+ except Exception:
209
+ pass
210
+ try:
211
+ _LOCK_FILE.unlink(missing_ok=True)
212
+ except Exception:
213
+ pass
214
+
215
+
216
+ def _wait_for_daemon(timeout: int = 60) -> bool:
217
+ """Wait for daemon to become reachable. Checks PID alive first (fast),
218
+ then health endpoint (confirms HTTP server is bound)."""
219
+ for _ in range(timeout * 2): # check every 0.5s
220
+ time.sleep(0.5)
221
+ if is_daemon_running():
222
+ # PID is alive — now optionally check if HTTP is ready
223
+ port = _get_port()
224
+ try:
225
+ import urllib.request
226
+ urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=2)
227
+ return True # HTTP is ready
228
+ except Exception:
229
+ # PID alive but HTTP not ready — daemon is warming up, that's OK
230
+ return True
126
231
  return False
127
232
 
128
233
 
129
234
  def stop_daemon() -> bool:
130
- """Stop the running daemon gracefully."""
235
+ """Stop the running daemon gracefully.
236
+
237
+ v3.4.3: Uses psutil for cross-platform process termination.
238
+ Falls back to os.kill if psutil unavailable.
239
+ """
131
240
  if not _PID_FILE.exists():
132
241
  return True
133
242
  try:
134
243
  pid = int(_PID_FILE.read_text().strip())
135
- os.kill(pid, signal.SIGTERM)
136
- # Wait for cleanup
137
- for _ in range(20):
138
- time.sleep(0.5)
139
- try:
140
- os.kill(pid, 0)
141
- except ProcessLookupError:
142
- break
244
+
245
+ # Cross-platform termination via psutil
246
+ try:
247
+ import psutil
248
+ proc = psutil.Process(pid)
249
+ proc.terminate() # SIGTERM on Unix, TerminateProcess on Windows
250
+ proc.wait(timeout=10)
251
+ except ImportError:
252
+ # Fallback: direct signal (works on Unix, may fail on Windows)
253
+ os.kill(pid, signal.SIGTERM)
254
+ for _ in range(20):
255
+ time.sleep(0.5)
256
+ try:
257
+ os.kill(pid, 0)
258
+ except ProcessLookupError:
259
+ break
260
+ except Exception:
261
+ pass
262
+
143
263
  _PID_FILE.unlink(missing_ok=True)
144
264
  _PORT_FILE.unlink(missing_ok=True)
145
265
  return True
@@ -195,8 +195,8 @@ def main() -> None:
195
195
  serve_p = sub.add_parser("serve", help="Start/stop daemon for instant CLI response (~600MB RAM)")
196
196
  serve_p.add_argument(
197
197
  "action", nargs="?", default="start",
198
- choices=["start", "stop", "status"],
199
- help="start (default), stop, or status",
198
+ choices=["start", "stop", "status", "install", "uninstall"],
199
+ help="start (default), stop, status, install (OS service), uninstall",
200
200
  )
201
201
 
202
202
  # -- Profiles ------------------------------------------------------
@@ -272,6 +272,16 @@ def main() -> None:
272
272
  )
273
273
  reap_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
274
274
 
275
+ # V3.4.3: Ingestion adapters
276
+ adapters_p = sub.add_parser(
277
+ "adapters",
278
+ help="Manage ingestion adapters (Gmail, Calendar, Transcript)",
279
+ )
280
+ adapters_p.add_argument(
281
+ "rest", nargs="*", default=[],
282
+ help="Subcommand: list, enable, disable, start, stop, status [name]",
283
+ )
284
+
275
285
  args = parser.parse_args()
276
286
 
277
287
  if not args.command:
@@ -282,6 +292,19 @@ def main() -> None:
282
292
  from superlocalmemory.cli.setup_wizard import check_first_use
283
293
  check_first_use(args.command)
284
294
 
295
+ # V3.4.4: Auto-start daemon for all commands that need it.
296
+ # SLM is always-on — close laptop, reboot, crash: daemon auto-recovers.
297
+ # Cross-platform: macOS + Windows + Linux.
298
+ _NO_DAEMON_COMMANDS = {
299
+ "setup", "mode", "provider", "connect", "migrate", "mcp", "warmup",
300
+ }
301
+ if args.command not in _NO_DAEMON_COMMANDS:
302
+ try:
303
+ from superlocalmemory.cli.daemon import ensure_daemon
304
+ ensure_daemon() # Starts daemon if not running; no-op if already up
305
+ except Exception:
306
+ pass # Don't block CLI if daemon start fails — commands have fallbacks
307
+
285
308
  from superlocalmemory.cli.commands import dispatch
286
309
 
287
310
  dispatch(args)