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.
- package/README.md +9 -12
- package/package.json +1 -1
- package/pyproject.toml +11 -2
- package/scripts/postinstall.js +26 -7
- package/src/superlocalmemory/cli/commands.py +71 -60
- package/src/superlocalmemory/cli/daemon.py +184 -64
- package/src/superlocalmemory/cli/main.py +25 -2
- package/src/superlocalmemory/cli/service_installer.py +367 -0
- package/src/superlocalmemory/cli/setup_wizard.py +150 -9
- package/src/superlocalmemory/core/config.py +28 -0
- package/src/superlocalmemory/core/consolidation_engine.py +38 -1
- package/src/superlocalmemory/core/engine.py +9 -0
- package/src/superlocalmemory/core/health_monitor.py +313 -0
- package/src/superlocalmemory/core/reranker_worker.py +19 -5
- package/src/superlocalmemory/ingestion/__init__.py +13 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +234 -0
- package/src/superlocalmemory/ingestion/base_adapter.py +177 -0
- package/src/superlocalmemory/ingestion/calendar_adapter.py +340 -0
- package/src/superlocalmemory/ingestion/credentials.py +118 -0
- package/src/superlocalmemory/ingestion/gmail_adapter.py +369 -0
- package/src/superlocalmemory/ingestion/parsers.py +100 -0
- package/src/superlocalmemory/ingestion/transcript_adapter.py +156 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +47 -1
- package/src/superlocalmemory/learning/entity_compiler.py +377 -0
- package/src/superlocalmemory/mcp/server.py +32 -3
- package/src/superlocalmemory/mcp/tools_mesh.py +249 -0
- package/src/superlocalmemory/mesh/__init__.py +12 -0
- package/src/superlocalmemory/mesh/broker.py +344 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +12 -6
- package/src/superlocalmemory/server/api.py +6 -7
- package/src/superlocalmemory/server/routes/adapters.py +63 -0
- package/src/superlocalmemory/server/routes/entity.py +151 -0
- package/src/superlocalmemory/server/routes/ingest.py +110 -0
- package/src/superlocalmemory/server/routes/mesh.py +186 -0
- package/src/superlocalmemory/server/unified_daemon.py +693 -0
- package/src/superlocalmemory/storage/schema_v343.py +229 -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 +18 -14
- package/src/superlocalmemory.egg-info/SOURCES.txt +26 -0
- 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.
|
|
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">
|
|
@@ -340,20 +340,17 @@ slm dashboard # Opens at http://localhost:8765
|
|
|
340
340
|
```
|
|
341
341
|
|
|
342
342
|
<details open>
|
|
343
|
-
<summary><strong>Dashboard
|
|
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
|
-
<
|
|
347
|
-
<
|
|
348
|
-
</
|
|
349
|
-
|
|
350
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/scripts/postinstall.js
CHANGED
|
@@ -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 —
|
|
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 "..."
|
|
288
|
-
console.log(' slm recall "..."
|
|
289
|
-
console.log(' slm dashboard
|
|
290
|
-
console.log(' slm
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
-
|
|
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("
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1227
|
-
|
|
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 —
|
|
5
|
+
"""SLM Daemon — client functions for communicating with the unified daemon.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
56
|
-
"""
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
except
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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,
|
|
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)
|