superlocalmemory 3.4.0 → 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.
Files changed (63) hide show
  1. package/README.md +7 -8
  2. package/docs/screenshots/01-dashboard-main.png +0 -0
  3. package/docs/screenshots/02-knowledge-graph.png +0 -0
  4. package/docs/screenshots/03-patterns-learning.png +0 -0
  5. package/docs/screenshots/04-learning-dashboard.png +0 -0
  6. package/docs/screenshots/05-behavioral-analysis.png +0 -0
  7. package/docs/screenshots/06-graph-communities.png +0 -0
  8. package/package.json +2 -2
  9. package/pyproject.toml +11 -2
  10. package/scripts/postinstall.js +26 -7
  11. package/src/superlocalmemory/cli/commands.py +42 -60
  12. package/src/superlocalmemory/cli/daemon.py +107 -47
  13. package/src/superlocalmemory/cli/main.py +10 -0
  14. package/src/superlocalmemory/cli/setup_wizard.py +137 -9
  15. package/src/superlocalmemory/core/config.py +28 -0
  16. package/src/superlocalmemory/core/consolidation_engine.py +38 -1
  17. package/src/superlocalmemory/core/engine.py +9 -0
  18. package/src/superlocalmemory/core/engine_wiring.py +5 -1
  19. package/src/superlocalmemory/core/graph_analyzer.py +254 -12
  20. package/src/superlocalmemory/core/health_monitor.py +313 -0
  21. package/src/superlocalmemory/core/reranker_worker.py +19 -5
  22. package/src/superlocalmemory/ingestion/__init__.py +13 -0
  23. package/src/superlocalmemory/ingestion/adapter_manager.py +234 -0
  24. package/src/superlocalmemory/ingestion/base_adapter.py +177 -0
  25. package/src/superlocalmemory/ingestion/calendar_adapter.py +340 -0
  26. package/src/superlocalmemory/ingestion/credentials.py +118 -0
  27. package/src/superlocalmemory/ingestion/gmail_adapter.py +369 -0
  28. package/src/superlocalmemory/ingestion/parsers.py +100 -0
  29. package/src/superlocalmemory/ingestion/transcript_adapter.py +156 -0
  30. package/src/superlocalmemory/learning/consolidation_worker.py +287 -53
  31. package/src/superlocalmemory/learning/entity_compiler.py +377 -0
  32. package/src/superlocalmemory/mesh/__init__.py +12 -0
  33. package/src/superlocalmemory/mesh/broker.py +344 -0
  34. package/src/superlocalmemory/retrieval/entity_channel.py +141 -4
  35. package/src/superlocalmemory/retrieval/spreading_activation.py +45 -0
  36. package/src/superlocalmemory/server/api.py +15 -8
  37. package/src/superlocalmemory/server/routes/behavioral.py +8 -4
  38. package/src/superlocalmemory/server/routes/chat.py +320 -0
  39. package/src/superlocalmemory/server/routes/entity.py +95 -0
  40. package/src/superlocalmemory/server/routes/ingest.py +110 -0
  41. package/src/superlocalmemory/server/routes/insights.py +368 -0
  42. package/src/superlocalmemory/server/routes/learning.py +106 -6
  43. package/src/superlocalmemory/server/routes/memories.py +20 -9
  44. package/src/superlocalmemory/server/routes/mesh.py +186 -0
  45. package/src/superlocalmemory/server/routes/stats.py +25 -3
  46. package/src/superlocalmemory/server/routes/timeline.py +252 -0
  47. package/src/superlocalmemory/server/routes/v3_api.py +161 -0
  48. package/src/superlocalmemory/server/ui.py +8 -0
  49. package/src/superlocalmemory/server/unified_daemon.py +691 -0
  50. package/src/superlocalmemory/storage/schema_v343.py +229 -0
  51. package/src/superlocalmemory/ui/index.html +168 -58
  52. package/src/superlocalmemory/ui/js/graph-event-bus.js +83 -0
  53. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  54. package/src/superlocalmemory/ui/js/knowledge-graph.js +942 -0
  55. package/src/superlocalmemory/ui/js/memory-chat.js +344 -0
  56. package/src/superlocalmemory/ui/js/memory-timeline.js +265 -0
  57. package/src/superlocalmemory/ui/js/quick-actions.js +334 -0
  58. package/src/superlocalmemory.egg-info/PKG-INFO +0 -594
  59. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -279
  60. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  61. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  62. package/src/superlocalmemory.egg-info/requires.txt +0 -47
  63. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
package/README.md CHANGED
@@ -341,20 +341,19 @@ slm dashboard # Opens at http://localhost:8765
341
341
 
342
342
  <details open>
343
343
  <summary><strong>Dashboard Screenshots</strong> (click to collapse)</summary>
344
- <p align="center"><img src="docs/screenshots/01-dashboard-main.png" alt="Dashboard" width="600"/></p>
344
+ <p align="center"><img src="docs/screenshots/01-dashboard-main.png" alt="Dashboard Overview — 3,100+ memories, 430K connections" width="600"/></p>
345
345
  <p align="center">
346
- <img src="docs/screenshots/02-knowledge-graph.png" alt="Graph" width="190"/>
347
- <img src="docs/screenshots/03-math-health.png" alt="Math" width="190"/>
348
- <img src="docs/screenshots/05-trust-dashboard.png" alt="Trust" width="190"/>
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"/>
349
348
  </p>
350
349
  <p align="center">
351
- <img src="docs/screenshots/04-recall-lab.png" alt="Recall" width="190"/>
352
- <img src="docs/screenshots/06-settings.png" alt="Settings" width="190"/>
353
- <img src="docs/screenshots/07-memories-blurred.png" alt="Memories" width="190"/>
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"/>
354
353
  </p>
355
354
  </details>
356
355
 
357
- 23 tabs: Dashboard, Recall Lab, Knowledge Graph, Memories, Trust Scores, Math Health, Compliance, Learning, IDE Connections, Settings, Memory Lifecycle, Compression, Patterns, and more. Runs locally — no data leaves your machine.
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.
358
357
 
359
358
  ---
360
359
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.4.0",
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",
@@ -89,4 +89,4 @@
89
89
  "dependencies": {
90
90
  "docx": "^9.5.1"
91
91
  }
92
- }
92
+ }
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.4.0"
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",
@@ -15,7 +15,7 @@ const os = require('os');
15
15
  const fs = require('fs');
16
16
 
17
17
  console.log('\n════════════════════════════════════════════════════════════');
18
- console.log(' SuperLocalMemory V3 — Post-Installation');
18
+ console.log(' SuperLocalMemory V3 — The Unified Brain');
19
19
  console.log(' by Varun Pratap Bhardwaj / Qualixar');
20
20
  console.log(' https://github.com/qualixar/superlocalmemory');
21
21
  console.log('════════════════════════════════════════════════════════════\n');
@@ -164,6 +164,17 @@ if (pipInstall(perfDeps, 'performance')) {
164
164
  console.log('⚠ Performance deps skipped (system works fine without them).');
165
165
  }
166
166
 
167
+ // V3.4.3: Unified Brain dependencies (health monitor, structured logging, file watching)
168
+ const brainDeps = ['psutil>=5.9.0', 'structlog>=24.0.0', 'watchdog>=4.0.0'];
169
+ console.log('\nInstalling Unified Brain dependencies (health monitor, file watcher)...');
170
+ if (pipInstall(brainDeps, 'brain')) {
171
+ console.log('✓ Unified Brain deps installed (psutil + structlog + watchdog)');
172
+ console.log(' Health monitoring, structured logging, and file watching enabled');
173
+ } else {
174
+ console.log('⚠ Unified Brain deps partially installed (health monitoring may be limited).');
175
+ console.log(' Run manually: pip install psutil structlog watchdog');
176
+ }
177
+
167
178
  // --- Step 3b: Install the superlocalmemory package itself ---
168
179
  // This ensures `python -m superlocalmemory.cli.main` always resolves the
169
180
  // correct version, even when invoked outside the Node.js wrapper (e.g.,
@@ -281,13 +292,21 @@ if (setupResult.status === 0) {
281
292
 
282
293
  // --- Done ---
283
294
  console.log('\n════════════════════════════════════════════════════════════');
284
- console.log(' ✓ SuperLocalMemory V3 installed!');
295
+ console.log(' ✓ SuperLocalMemory V3 — The Unified Brain installed!');
285
296
  console.log('');
286
297
  console.log(' Quick start:');
287
- console.log(' slm remember "..." # Store a memory');
288
- console.log(' slm recall "..." # Search memories');
289
- console.log(' slm dashboard # Open web dashboard');
290
- console.log(' slm setup # Re-run setup wizard');
298
+ console.log(' slm remember "..." # Store a memory');
299
+ console.log(' slm recall "..." # Search memories');
300
+ console.log(' slm dashboard # Open web dashboard');
301
+ console.log(' slm serve # Start 24/7 daemon');
302
+ console.log(' slm adapters enable gmail # Enable Gmail ingestion');
303
+ console.log(' slm setup # Re-run 9-step wizard');
304
+ console.log('');
305
+ console.log(' New in v3.4.3:');
306
+ console.log(' • Unified daemon (one process, 24/7, < 700MB)');
307
+ console.log(' • SLM Mesh (agent-to-agent P2P built in)');
308
+ console.log(' • Entity compilation (auto knowledge summaries)');
309
+ console.log(' • Ingestion adapters (Gmail, Calendar, Transcripts)');
291
310
  console.log('');
292
- console.log(' Docs: https://github.com/qualixar/superlocalmemory/wiki');
311
+ console.log(' Docs: https://github.com/qualixar/superlocalmemory');
293
312
  console.log('════════════════════════════════════════════════════════════\n');
@@ -58,6 +58,8 @@ def dispatch(args: Namespace) -> None:
58
58
  "reap": cmd_reap,
59
59
  # V3.3.21 daemon
60
60
  "serve": cmd_serve,
61
+ # V3.4.3 ingestion adapters
62
+ "adapters": cmd_adapters,
61
63
  }
62
64
  handler = handlers.get(args.command)
63
65
  if handler:
@@ -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
- """Launch the web dashboard."""
1163
- try:
1164
- import uvicorn
1165
- except ImportError:
1166
- print("Dashboard requires additional deps. Run: slm doctor")
1167
- print("Or install manually: pip install 'fastapi[all]' uvicorn")
1168
- sys.exit(1)
1169
-
1170
- import os
1171
- import signal
1172
- import socket
1184
+ """Open the web dashboard in the browser.
1173
1185
 
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.
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
- # Brief wait for port to fully release after killing old process
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("=" * 60)
1220
- print(f" Dashboard: http://localhost:{port}")
1221
- print(f" API Docs: http://localhost:{port}/api/docs")
1222
- print(" Press Ctrl+C to stop\n")
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
- from superlocalmemory.server.ui import create_app
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
- app = create_app()
1227
- uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")
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 — keeps engine warm for instant CLI/MCP response.
5
+ """SLM Daemon — client functions for communicating with the unified daemon.
6
6
 
7
- Problem: CLI cold start is 23s (embedding worker spawn + model load).
8
- Solution: Background daemon keeps MemoryEngine warm. CLI commands route
9
- requests through the daemon via localhost HTTP (~10ms overhead).
7
+ The unified daemon (server/unified_daemon.py) runs as a single FastAPI/uvicorn
8
+ process on port 8765, with port 8767 as a backward-compat TCP redirect.
10
9
 
11
- Architecture:
12
- slm serve → starts daemon (engine init, workers warm, ~600MB RAM)
13
- slm remember X → HTTP POST to daemon instant (no cold start)
14
- slm recall X → HTTP GET from daemon → instant
15
- slm serve stop → graceful shutdown, workers killed, RAM freed
10
+ This module contains CLIENT functions used by CLI commands:
11
+ - is_daemon_running(): check if daemon is alive
12
+ - ensure_daemon(): start daemon if not running
13
+ - stop_daemon(): gracefully stop the daemon
14
+ - daemon_request(): send HTTP request to daemon
16
15
 
17
- Auto-start: if daemon not running on CLI use, starts it automatically.
18
- Auto-shutdown: after 30 min idle (configurable via SLM_DAEMON_IDLE_TIMEOUT).
19
-
20
- Memory safety:
21
- - RSS watchdog on embedding worker (2.5GB cap)
22
- - Worker recycling every 5000 requests
23
- - Parent watchdog kills workers if daemon dies
24
- - SQLite WAL mode for concurrent access
16
+ The actual daemon server code is in server/unified_daemon.py.
25
17
 
26
18
  Part of Qualixar | Author: Varun Pratap Bhardwaj
27
19
  License: Elastic-2.0
@@ -42,8 +34,9 @@ from threading import Thread
42
34
 
43
35
  logger = logging.getLogger(__name__)
44
36
 
45
- _DEFAULT_PORT = 8767
46
- _DEFAULT_IDLE_TIMEOUT = 1800 # 30 min
37
+ _DEFAULT_PORT = 8765 # v3.4.3: unified daemon on 8765 (was 8767)
38
+ _LEGACY_PORT = 8767 # backward-compat redirect
39
+ _DEFAULT_IDLE_TIMEOUT = 0 # v3.4.3: 24/7 default (was 1800)
47
40
  _PID_FILE = Path.home() / ".superlocalmemory" / "daemon.pid"
48
41
  _PORT_FILE = Path.home() / ".superlocalmemory" / "daemon.port"
49
42
 
@@ -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
- os.kill(pid, 0) # Check if process exists
62
- except (ValueError, ProcessLookupError, PermissionError):
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
- try:
69
- import urllib.request
70
- resp = urllib.request.urlopen(
71
- f"http://127.0.0.1:{port}/health", timeout=2,
72
- )
73
- return resp.status == 200
74
- except Exception:
75
- return False
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.cli.daemon", "--start"]
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
- os.kill(pid, signal.SIGTERM)
136
- # Wait for cleanup
137
- for _ in range(20):
138
- time.sleep(0.5)
139
- try:
140
- os.kill(pid, 0)
141
- except ProcessLookupError:
142
- break
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: