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 +7 -17
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/cli/commands.py +29 -0
- package/src/superlocalmemory/cli/daemon.py +128 -68
- package/src/superlocalmemory/cli/main.py +15 -2
- package/src/superlocalmemory/cli/service_installer.py +367 -0
- package/src/superlocalmemory/cli/setup_wizard.py +13 -0
- package/src/superlocalmemory/mcp/server.py +32 -3
- package/src/superlocalmemory/mcp/tools_mesh.py +249 -0
- package/src/superlocalmemory/server/routes/adapters.py +63 -0
- package/src/superlocalmemory/server/routes/entity.py +56 -0
- package/src/superlocalmemory/server/unified_daemon.py +2 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +1588 -0
- package/src/superlocalmemory/ui/index.html +134 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +28 -1
- package/src/superlocalmemory/ui/js/ng-entities.js +272 -0
- package/src/superlocalmemory/ui/js/ng-health.js +208 -0
- package/src/superlocalmemory/ui/js/ng-ingestion.js +203 -0
- package/src/superlocalmemory/ui/js/ng-mesh.js +311 -0
- package/src/superlocalmemory/ui/js/ng-shell.js +471 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +601 -0
- package/src/superlocalmemory.egg-info/SOURCES.txt +313 -0
- package/src/superlocalmemory.egg-info/dependency_links.txt +1 -0
- package/src/superlocalmemory.egg-info/entry_points.txt +2 -0
- package/src/superlocalmemory.egg-info/requires.txt +55 -0
- package/src/superlocalmemory.egg-info/top_level.txt +1 -0
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.
|
|
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.
|
|
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
|
-
|
|
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
|
+
"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
|
@@ -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.
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
84
|
-
if
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
for try_port in (
|
|
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):
|
|
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
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
143
|
-
|
|
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
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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,
|
|
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)
|