superlocalmemory 3.4.1 → 3.4.3
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/package.json +1 -1
- package/pyproject.toml +11 -2
- package/scripts/postinstall.js +26 -7
- package/src/superlocalmemory/cli/commands.py +42 -60
- package/src/superlocalmemory/cli/daemon.py +107 -47
- package/src/superlocalmemory/cli/main.py +10 -0
- package/src/superlocalmemory/cli/setup_wizard.py +137 -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/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/entity.py +95 -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 +691 -0
- package/src/superlocalmemory/storage/schema_v343.py +229 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -597
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -287
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -47
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.3",
|
|
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.3"
|
|
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:
|
|
@@ -110,6 +112,26 @@ def cmd_serve(args: Namespace) -> None:
|
|
|
110
112
|
print("Failed to start daemon. Check ~/.superlocalmemory/logs/daemon.log")
|
|
111
113
|
|
|
112
114
|
|
|
115
|
+
# -- Ingestion Adapters (V3.4.3) ------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_adapters(args: Namespace) -> None:
|
|
119
|
+
"""Manage ingestion adapters (Gmail, Calendar, Transcript).
|
|
120
|
+
|
|
121
|
+
Usage:
|
|
122
|
+
slm adapters list — show all adapters
|
|
123
|
+
slm adapters enable <name> — enable an adapter
|
|
124
|
+
slm adapters disable <name> — disable and stop
|
|
125
|
+
slm adapters start <name> — start running
|
|
126
|
+
slm adapters stop <name> — stop running
|
|
127
|
+
slm adapters status — detailed status
|
|
128
|
+
"""
|
|
129
|
+
from superlocalmemory.ingestion.adapter_manager import handle_adapters_cli
|
|
130
|
+
# args.rest contains everything after "adapters"
|
|
131
|
+
rest = getattr(args, 'rest', []) or []
|
|
132
|
+
handle_adapters_cli(rest)
|
|
133
|
+
|
|
134
|
+
|
|
113
135
|
# -- Setup & Config (no --json — interactive commands) ---------------------
|
|
114
136
|
|
|
115
137
|
|
|
@@ -1159,72 +1181,32 @@ def _warmup_diagnose() -> None:
|
|
|
1159
1181
|
|
|
1160
1182
|
|
|
1161
1183
|
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
|
|
1184
|
+
"""Open the web dashboard in the browser.
|
|
1173
1185
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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)
|
|
1186
|
+
v3.4.3: Dashboard is now served by the unified daemon. This command
|
|
1187
|
+
ensures the daemon is running and opens the browser. It does NOT
|
|
1188
|
+
start a separate server (saves ~500MB RAM from duplicate engine).
|
|
1189
|
+
"""
|
|
1190
|
+
from superlocalmemory.cli.daemon import ensure_daemon, _get_port
|
|
1212
1191
|
|
|
1213
|
-
|
|
1214
|
-
import time
|
|
1215
|
-
time.sleep(1)
|
|
1192
|
+
port = getattr(args, "port", None) or _get_port()
|
|
1216
1193
|
|
|
1217
|
-
print("=" * 60)
|
|
1218
1194
|
print(" SuperLocalMemory V3 — Web Dashboard")
|
|
1219
|
-
print("
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1195
|
+
print(f" Starting daemon if needed...")
|
|
1196
|
+
|
|
1197
|
+
if not ensure_daemon():
|
|
1198
|
+
print(" ✗ Could not start daemon. Run `slm doctor` to diagnose.")
|
|
1199
|
+
sys.exit(1)
|
|
1223
1200
|
|
|
1224
|
-
|
|
1201
|
+
url = f"http://localhost:{port}"
|
|
1202
|
+
print(f" ✓ Daemon running")
|
|
1203
|
+
print(f" Dashboard: {url}")
|
|
1204
|
+
print(f" API Docs: {url}/docs")
|
|
1225
1205
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1206
|
+
# Open browser
|
|
1207
|
+
import webbrowser
|
|
1208
|
+
webbrowser.open(url)
|
|
1209
|
+
print("\n Dashboard opened in browser. Daemon continues running in background.")
|
|
1228
1210
|
|
|
1229
1211
|
|
|
1230
1212
|
# -- 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
|
|
|
@@ -53,26 +46,70 @@ _PORT_FILE = Path.home() / ".superlocalmemory" / "daemon.port"
|
|
|
53
46
|
# ---------------------------------------------------------------------------
|
|
54
47
|
|
|
55
48
|
def is_daemon_running() -> bool:
|
|
56
|
-
"""Check if daemon is alive via PID file + HTTP health check.
|
|
49
|
+
"""Check if daemon is alive via PID file + HTTP health check.
|
|
50
|
+
|
|
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).
|
|
53
|
+
"""
|
|
57
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
|
|
58
78
|
return False
|
|
59
79
|
try:
|
|
60
80
|
pid = int(_PID_FILE.read_text().strip())
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
# Cross-platform PID check via psutil if available, else os.kill
|
|
82
|
+
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):
|
|
91
|
+
_PID_FILE.unlink(missing_ok=True)
|
|
92
|
+
return False
|
|
93
|
+
except ValueError:
|
|
63
94
|
_PID_FILE.unlink(missing_ok=True)
|
|
64
95
|
return False
|
|
65
96
|
|
|
66
|
-
# PID exists — verify HTTP health
|
|
97
|
+
# PID exists — verify HTTP health on primary port
|
|
67
98
|
port = _get_port()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
for try_port in (port, _DEFAULT_PORT, _LEGACY_PORT):
|
|
100
|
+
try:
|
|
101
|
+
import urllib.request
|
|
102
|
+
resp = urllib.request.urlopen(
|
|
103
|
+
f"http://127.0.0.1:{try_port}/health", timeout=2,
|
|
104
|
+
)
|
|
105
|
+
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))
|
|
109
|
+
return True
|
|
110
|
+
except Exception:
|
|
111
|
+
continue
|
|
112
|
+
return False
|
|
76
113
|
|
|
77
114
|
|
|
78
115
|
def _get_port() -> int:
|
|
@@ -100,22 +137,30 @@ def daemon_request(method: str, path: str, body: dict | None = None) -> dict | N
|
|
|
100
137
|
|
|
101
138
|
|
|
102
139
|
def ensure_daemon() -> bool:
|
|
103
|
-
"""Start daemon if not running. Returns True if daemon is ready.
|
|
140
|
+
"""Start daemon if not running. Returns True if daemon is ready.
|
|
141
|
+
|
|
142
|
+
v3.4.3: Starts unified daemon (server/unified_daemon.py) instead of
|
|
143
|
+
old stdlib daemon. Cross-platform subprocess flags.
|
|
144
|
+
"""
|
|
104
145
|
if is_daemon_running():
|
|
105
146
|
return True
|
|
106
147
|
|
|
107
|
-
# Start daemon in background
|
|
148
|
+
# Start unified daemon in background
|
|
108
149
|
import subprocess
|
|
109
|
-
cmd = [sys.executable, "-m", "superlocalmemory.
|
|
150
|
+
cmd = [sys.executable, "-m", "superlocalmemory.server.unified_daemon", "--start"]
|
|
110
151
|
log_dir = Path.home() / ".superlocalmemory" / "logs"
|
|
111
152
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
112
153
|
log_file = log_dir / "daemon.log"
|
|
113
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
|
|
161
|
+
|
|
114
162
|
with open(log_file, "a") as lf:
|
|
115
|
-
subprocess.Popen(
|
|
116
|
-
cmd, stdout=lf, stderr=lf,
|
|
117
|
-
start_new_session=True,
|
|
118
|
-
)
|
|
163
|
+
subprocess.Popen(cmd, stdout=lf, stderr=lf, **kwargs)
|
|
119
164
|
|
|
120
165
|
# Wait for daemon to become ready (max 30s for cold start)
|
|
121
166
|
for _ in range(60):
|
|
@@ -127,19 +172,34 @@ def ensure_daemon() -> bool:
|
|
|
127
172
|
|
|
128
173
|
|
|
129
174
|
def stop_daemon() -> bool:
|
|
130
|
-
"""Stop the running daemon gracefully.
|
|
175
|
+
"""Stop the running daemon gracefully.
|
|
176
|
+
|
|
177
|
+
v3.4.3: Uses psutil for cross-platform process termination.
|
|
178
|
+
Falls back to os.kill if psutil unavailable.
|
|
179
|
+
"""
|
|
131
180
|
if not _PID_FILE.exists():
|
|
132
181
|
return True
|
|
133
182
|
try:
|
|
134
183
|
pid = int(_PID_FILE.read_text().strip())
|
|
135
|
-
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
184
|
+
|
|
185
|
+
# Cross-platform termination via psutil
|
|
186
|
+
try:
|
|
187
|
+
import psutil
|
|
188
|
+
proc = psutil.Process(pid)
|
|
189
|
+
proc.terminate() # SIGTERM on Unix, TerminateProcess on Windows
|
|
190
|
+
proc.wait(timeout=10)
|
|
191
|
+
except ImportError:
|
|
192
|
+
# Fallback: direct signal (works on Unix, may fail on Windows)
|
|
193
|
+
os.kill(pid, signal.SIGTERM)
|
|
194
|
+
for _ in range(20):
|
|
195
|
+
time.sleep(0.5)
|
|
196
|
+
try:
|
|
197
|
+
os.kill(pid, 0)
|
|
198
|
+
except ProcessLookupError:
|
|
199
|
+
break
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
143
203
|
_PID_FILE.unlink(missing_ok=True)
|
|
144
204
|
_PORT_FILE.unlink(missing_ok=True)
|
|
145
205
|
return True
|
|
@@ -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:
|
|
@@ -259,7 +259,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
259
259
|
|
|
260
260
|
print()
|
|
261
261
|
print("╔══════════════════════════════════════════════════════════╗")
|
|
262
|
-
print("║ SuperLocalMemory V3 —
|
|
262
|
+
print("║ SuperLocalMemory V3 — The Unified Brain ║")
|
|
263
263
|
print("║ by Varun Pratap Bhardwaj / Qualixar ║")
|
|
264
264
|
print("╚══════════════════════════════════════════════════════════╝")
|
|
265
265
|
print()
|
|
@@ -373,9 +373,9 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
373
373
|
else:
|
|
374
374
|
print(f"\n ✓ CodeGraph disabled (enable later in {cg_config_path})")
|
|
375
375
|
|
|
376
|
-
# -- Step 4: Download
|
|
376
|
+
# -- Step 4: Download models --
|
|
377
377
|
print()
|
|
378
|
-
print("─── Step 4/
|
|
378
|
+
print("─── Step 4/9: Download Embedding Model ───")
|
|
379
379
|
|
|
380
380
|
if not st_ok:
|
|
381
381
|
print(" ⚠ Skipped (sentence-transformers not installed)")
|
|
@@ -385,18 +385,129 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
385
385
|
if not embed_ok:
|
|
386
386
|
print(" ⚠ Model will download on first use (may take a few minutes)")
|
|
387
387
|
|
|
388
|
-
# -- Step 4: Download reranker model --
|
|
389
388
|
print()
|
|
390
|
-
print("─── Step
|
|
389
|
+
print("─── Step 4b/9: Download Reranker Model ───")
|
|
391
390
|
|
|
392
391
|
if not st_ok:
|
|
393
392
|
print(" ⚠ Skipped (sentence-transformers not installed)")
|
|
394
393
|
else:
|
|
395
394
|
_download_reranker(_RERANKER_MODEL)
|
|
396
395
|
|
|
397
|
-
# -- Step 5:
|
|
396
|
+
# -- Step 5: Daemon Configuration (v3.4.3) --
|
|
397
|
+
print()
|
|
398
|
+
print("─── Step 5/9: Daemon Configuration ───")
|
|
399
|
+
print()
|
|
400
|
+
print(" The SLM daemon runs in the background for instant memory access.")
|
|
401
|
+
print()
|
|
402
|
+
print(" [1] 24/7 Always-On (recommended — brain never sleeps)")
|
|
403
|
+
print(" [2] Auto-shutdown after idle (saves RAM when not coding)")
|
|
404
|
+
print()
|
|
405
|
+
|
|
406
|
+
if interactive:
|
|
407
|
+
daemon_choice = _prompt(" Select daemon mode [1/2] (default: 1): ", "1")
|
|
408
|
+
else:
|
|
409
|
+
daemon_choice = "1"
|
|
410
|
+
print(" Auto-selecting 24/7 mode (non-interactive)")
|
|
411
|
+
|
|
412
|
+
if daemon_choice == "2":
|
|
413
|
+
if interactive:
|
|
414
|
+
timeout_choice = _prompt(" Idle timeout [30m/1h/2h] (default: 30m): ", "30m")
|
|
415
|
+
else:
|
|
416
|
+
timeout_choice = "30m"
|
|
417
|
+
timeout_map = {"30m": 1800, "1h": 3600, "2h": 7200}
|
|
418
|
+
config.daemon_idle_timeout = timeout_map.get(timeout_choice, 1800)
|
|
419
|
+
print(f"\n ✓ Auto-shutdown after {timeout_choice} idle")
|
|
420
|
+
else:
|
|
421
|
+
config.daemon_idle_timeout = 0
|
|
422
|
+
print("\n ✓ 24/7 Always-On mode")
|
|
423
|
+
|
|
424
|
+
config.save()
|
|
425
|
+
|
|
426
|
+
# -- Step 6: Mesh Communication (v3.4.3) --
|
|
427
|
+
print()
|
|
428
|
+
print("─── Step 6/9: Mesh Communication ───")
|
|
429
|
+
print()
|
|
430
|
+
print(" SLM Mesh enables agent-to-agent P2P communication.")
|
|
431
|
+
print(" Multiple AI sessions can share knowledge in real-time.")
|
|
432
|
+
print()
|
|
433
|
+
print(" [Y] Enable Mesh (recommended)")
|
|
434
|
+
print(" [N] Disable Mesh")
|
|
435
|
+
print()
|
|
436
|
+
|
|
437
|
+
if interactive:
|
|
438
|
+
mesh_choice = _prompt(" Enable Mesh? [Y/n] (default: Y): ", "y").lower()
|
|
439
|
+
else:
|
|
440
|
+
mesh_choice = "y"
|
|
441
|
+
print(" Auto-enabling Mesh (non-interactive)")
|
|
442
|
+
|
|
443
|
+
config.mesh_enabled = mesh_choice in ("", "y", "yes")
|
|
444
|
+
config.save()
|
|
445
|
+
print(f"\n ✓ Mesh {'enabled' if config.mesh_enabled else 'disabled'}")
|
|
446
|
+
|
|
447
|
+
# -- Step 7: Ingestion Adapters (v3.4.3) --
|
|
448
|
+
print()
|
|
449
|
+
print("─── Step 7/9: Ingestion Adapters ───")
|
|
450
|
+
print()
|
|
451
|
+
print(" These let SLM learn from your email, calendar, and meetings.")
|
|
452
|
+
print(" All adapters are OFF by default. You can enable them later.")
|
|
453
|
+
print()
|
|
454
|
+
print(" Available adapters:")
|
|
455
|
+
print(" • Gmail Ingestion — requires Google OAuth setup")
|
|
456
|
+
print(" • Google Calendar — shares Gmail credentials")
|
|
457
|
+
print(" • Meeting Transcripts — watches a folder for .srt/.vtt files")
|
|
458
|
+
print()
|
|
459
|
+
|
|
460
|
+
if interactive:
|
|
461
|
+
adapter_input = _prompt(" Enable any now? [Enter to skip, or type: gmail,calendar,transcript]: ", "")
|
|
462
|
+
else:
|
|
463
|
+
adapter_input = ""
|
|
464
|
+
|
|
465
|
+
# Save adapter preferences (actual setup happens via `slm adapters enable X`)
|
|
466
|
+
adapters_config = {"gmail": False, "calendar": False, "transcript": False}
|
|
467
|
+
if adapter_input:
|
|
468
|
+
for name in adapter_input.split(","):
|
|
469
|
+
name = name.strip().lower()
|
|
470
|
+
if name in adapters_config:
|
|
471
|
+
adapters_config[name] = True
|
|
472
|
+
|
|
473
|
+
adapters_path = _SLM_HOME / "adapters.json"
|
|
474
|
+
import json as _json
|
|
475
|
+
adapters_path.write_text(_json.dumps(
|
|
476
|
+
{k: {"enabled": v, "tier": "polling"} for k, v in adapters_config.items()},
|
|
477
|
+
indent=2,
|
|
478
|
+
))
|
|
479
|
+
|
|
480
|
+
enabled_adapters = [k for k, v in adapters_config.items() if v]
|
|
481
|
+
if enabled_adapters:
|
|
482
|
+
print(f"\n ✓ Enabled: {', '.join(enabled_adapters)}")
|
|
483
|
+
print(" Run `slm adapters start <name>` to begin ingestion")
|
|
484
|
+
else:
|
|
485
|
+
print("\n ✓ All adapters disabled (enable later: slm adapters enable gmail)")
|
|
486
|
+
|
|
487
|
+
# -- Step 8: Entity Compilation (v3.4.3) --
|
|
488
|
+
print()
|
|
489
|
+
print("─── Step 8/9: Entity Compilation ───")
|
|
490
|
+
print()
|
|
491
|
+
print(" Entity compilation builds knowledge summaries per person,")
|
|
492
|
+
print(" project, and concept. Runs automatically during consolidation.")
|
|
493
|
+
print()
|
|
494
|
+
print(" [Y] Enable entity compilation (recommended)")
|
|
495
|
+
print(" [N] Disable")
|
|
398
496
|
print()
|
|
399
|
-
|
|
497
|
+
|
|
498
|
+
if interactive:
|
|
499
|
+
ec_choice = _prompt(" Enable entity compilation? [Y/n] (default: Y): ", "y").lower()
|
|
500
|
+
else:
|
|
501
|
+
ec_choice = "y"
|
|
502
|
+
print(" Auto-enabling entity compilation (non-interactive)")
|
|
503
|
+
|
|
504
|
+
config.entity_compilation_enabled = ec_choice in ("", "y", "yes")
|
|
505
|
+
config.save()
|
|
506
|
+
print(f"\n ✓ Entity compilation {'enabled' if config.entity_compilation_enabled else 'disabled'}")
|
|
507
|
+
|
|
508
|
+
# -- Step 9: Verification --
|
|
509
|
+
print()
|
|
510
|
+
print("─── Step 9/9: Verification ───")
|
|
400
511
|
|
|
401
512
|
if st_ok:
|
|
402
513
|
verified = _verify_installation()
|
|
@@ -410,16 +521,33 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
410
521
|
print()
|
|
411
522
|
print("╔══════════════════════════════════════════════════════════╗")
|
|
412
523
|
if verified:
|
|
413
|
-
print("║ ✓ Setup Complete —
|
|
524
|
+
print("║ ✓ Setup Complete — The Unified Brain is ready! ║")
|
|
414
525
|
else:
|
|
415
526
|
print("║ ✓ Setup Complete — basic config saved ║")
|
|
416
527
|
print("║ Models will auto-download on first use ║")
|
|
417
528
|
print("╚══════════════════════════════════════════════════════════╝")
|
|
418
529
|
print()
|
|
530
|
+
|
|
531
|
+
# Summary of choices
|
|
532
|
+
daemon_mode = "24/7" if config.daemon_idle_timeout == 0 else f"auto-shutdown ({config.daemon_idle_timeout}s)"
|
|
533
|
+
print(f" Enabled: Mode {choice.upper()}, Daemon ({daemon_mode})", end="")
|
|
534
|
+
if config.mesh_enabled:
|
|
535
|
+
print(", Mesh", end="")
|
|
536
|
+
if config.entity_compilation_enabled:
|
|
537
|
+
print(", Entity Compilation", end="")
|
|
538
|
+
if code_graph_enabled:
|
|
539
|
+
print(", CodeGraph", end="")
|
|
540
|
+
print()
|
|
541
|
+
if enabled_adapters:
|
|
542
|
+
print(f" Adapters: {', '.join(enabled_adapters)}")
|
|
543
|
+
else:
|
|
544
|
+
print(" Adapters: none (enable via: slm adapters enable gmail)")
|
|
545
|
+
print()
|
|
419
546
|
print(" Quick start:")
|
|
420
547
|
print(' slm remember "your first memory"')
|
|
421
548
|
print(' slm recall "search query"')
|
|
422
|
-
print(" slm dashboard")
|
|
549
|
+
print(" slm dashboard → http://localhost:8765")
|
|
550
|
+
print(" slm adapters enable gmail → start Gmail ingestion")
|
|
423
551
|
print()
|
|
424
552
|
print(" Need help?")
|
|
425
553
|
print(" slm doctor — diagnose issues")
|