superlocalmemory 3.4.3 → 3.4.5

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.
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">
@@ -22,6 +22,10 @@
22
22
  <a href="#dual-interface-mcp--cli"><img src="https://img.shields.io/badge/CLI-Agent--Native-green?style=for-the-badge" alt="CLI Agent-Native"/></a>
23
23
  </p>
24
24
 
25
+ <p align="center">
26
+ <video src="https://github.com/user-attachments/assets/c3b54a1d-f62a-4ea7-bba7-900435e7b3ab" width="800" autoplay loop muted playsinline></video>
27
+ </p>
28
+
25
29
  ---
26
30
 
27
31
  ## Why SuperLocalMemory?
@@ -339,21 +343,7 @@ Built-in compliance tools: GDPR Article 15/17 export + complete erasure, tamper-
339
343
  slm dashboard # Opens at http://localhost:8765
340
344
  ```
341
345
 
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>
345
- <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"/>
353
- </p>
354
- </details>
355
-
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.
346
+ **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
347
 
358
348
  ---
359
349
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.4.3",
3
+ "version": "3.4.5",
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.3"
3
+ version = "3.4.5"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0-or-later"}
@@ -96,6 +96,35 @@ def cmd_serve(args: Namespace) -> None:
96
96
  print("Daemon: RUNNING (could not get status)")
97
97
  else:
98
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.")
99
128
  return
100
129
 
101
130
  # Default: start
@@ -45,67 +45,65 @@ _PORT_FILE = Path.home() / ".superlocalmemory" / "daemon.port"
45
45
  # Client: check if daemon running + send requests
46
46
  # ---------------------------------------------------------------------------
47
47
 
48
+ def _is_pid_alive(pid: int) -> bool:
49
+ """Cross-platform check if a process with given PID exists."""
50
+ try:
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
59
+
60
+
48
61
  def is_daemon_running() -> bool:
49
62
  """Check if daemon is alive via PID file + HTTP health check.
50
63
 
51
- v3.4.3: Checks both port 8765 (new) and 8767 (legacy) for upgrade compat.
52
- Also checks ports directly if PID file is missing (daemon started by MCP/hook).
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
53
72
  """
54
- if not _PID_FILE.exists():
55
- # PID file missing but daemon might still be running (started by MCP/hook)
56
- # Try health check on known ports
57
- for try_port in (_DEFAULT_PORT, _LEGACY_PORT):
58
- try:
59
- import urllib.request
60
- resp = urllib.request.urlopen(
61
- f"http://127.0.0.1:{try_port}/health", timeout=2,
62
- )
63
- if resp.status == 200:
64
- # Daemon is running without PID file — write one for future checks
65
- try:
66
- import json as _json
67
- data = _json.loads(resp.read().decode())
68
- pid = data.get("pid")
69
- if pid:
70
- _PID_FILE.parent.mkdir(parents=True, exist_ok=True)
71
- _PID_FILE.write_text(str(pid))
72
- _PORT_FILE.write_text(str(try_port))
73
- except Exception:
74
- pass
75
- return True
76
- except Exception:
77
- continue
78
- return False
79
- try:
80
- pid = int(_PID_FILE.read_text().strip())
81
- # Cross-platform PID check via psutil if available, else os.kill
73
+ if _PID_FILE.exists():
82
74
  try:
83
- import psutil
84
- if not psutil.pid_exists(pid):
85
- _PID_FILE.unlink(missing_ok=True)
86
- return False
87
- except ImportError:
88
- try:
89
- os.kill(pid, 0)
90
- except (ProcessLookupError, PermissionError):
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
91
82
  _PID_FILE.unlink(missing_ok=True)
92
- return False
93
- except ValueError:
94
- _PID_FILE.unlink(missing_ok=True)
95
- return False
83
+ _PORT_FILE.unlink(missing_ok=True)
84
+ except (ValueError, OSError):
85
+ _PID_FILE.unlink(missing_ok=True)
96
86
 
97
- # PID existsverify HTTP health on primary port
98
- port = _get_port()
99
- for try_port in (port, _DEFAULT_PORT, _LEGACY_PORT):
87
+ # No PID filemaybe 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):
100
90
  try:
101
91
  import urllib.request
102
92
  resp = urllib.request.urlopen(
103
93
  f"http://127.0.0.1:{try_port}/health", timeout=2,
104
94
  )
105
95
  if resp.status == 200:
106
- # Update port file if it was stale (upgrade from 8767 → 8765)
107
- if try_port != port:
108
- _PORT_FILE.write_text(str(try_port))
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
109
107
  return True
110
108
  except Exception:
111
109
  continue
@@ -136,38 +134,100 @@ def daemon_request(method: str, path: str, body: dict | None = None) -> dict | N
136
134
  return None
137
135
 
138
136
 
137
+ _LOCK_FILE = Path.home() / ".superlocalmemory" / "daemon.lock"
138
+
139
+
139
140
  def ensure_daemon() -> bool:
140
141
  """Start daemon if not running. Returns True if daemon is ready.
141
142
 
142
- v3.4.3: Starts unified daemon (server/unified_daemon.py) instead of
143
- old stdlib daemon. Cross-platform subprocess flags.
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
144
148
  """
145
149
  if is_daemon_running():
146
150
  return True
147
151
 
148
- # Start unified daemon in background
149
- import subprocess
150
- cmd = [sys.executable, "-m", "superlocalmemory.server.unified_daemon", "--start"]
151
- log_dir = Path.home() / ".superlocalmemory" / "logs"
152
- log_dir.mkdir(parents=True, exist_ok=True)
153
- log_file = log_dir / "daemon.log"
154
-
155
- # Cross-platform background process flags
156
- kwargs: dict = {}
157
- if sys.platform == "win32":
158
- kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
159
- else:
160
- kwargs["start_new_session"] = True
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")
161
157
 
162
- with open(log_file, "a") as lf:
163
- subprocess.Popen(cmd, stdout=lf, stderr=lf, **kwargs)
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)
164
174
 
165
- # Wait for daemon to become ready (max 30s for cold start)
166
- for _ in range(60):
167
- time.sleep(0.5)
175
+ # Re-check after acquiring lock (another process may have started it)
168
176
  if is_daemon_running():
169
177
  return True
170
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
171
231
  return False
172
232
 
173
233
 
@@ -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 ------------------------------------------------------
@@ -292,6 +292,19 @@ def main() -> None:
292
292
  from superlocalmemory.cli.setup_wizard import check_first_use
293
293
  check_first_use(args.command)
294
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
+
295
308
  from superlocalmemory.cli.commands import dispatch
296
309
 
297
310
  dispatch(args)