feat: sync with upstream — v1.2.0, in-app updates, Projects, modules
Brings the public tree back in line with the development repo after several weeks of drift caused by a stale publish include list. New: - In-app update path: GET /update/check compares the checkout against origin/main and POST /update/apply runs `ncp upgrade` detached (pull, rebuild, restart). The sidebar shows the version, checks on click, and offers an "update available" pill. - Projects: a project workspace groups chats and RAG documents, with per-project instructions and document retrieval scoped to the active project. Replaces the standalone Documents page. - modules/: auto-discovered feature plugins (mail, network) with their frontend counterparts and tests. - Memory curation runs in-process (synapse/memory/curator.py) on the chat model when a conversation goes idle. The separate memory service on :8001 is gone, along with the launcher lines that started it. Also: the KDE theme, panel and Promethean terminal assets, the full test suite, and VERSION 1.2.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Plank (Nexus primary-follow)
|
||||
Exec=/home/jon/.local/bin/plank-primary-watch.sh
|
||||
Hidden=false
|
||||
X-XFCE-Autostart-Override=true
|
||||
+15
-89
@@ -1,4 +1,3 @@
|
||||
# /home/jon/nexus-core/management/controlpanel.py
|
||||
|
||||
import time
|
||||
import tkinter as tk
|
||||
@@ -20,7 +19,9 @@ try:
|
||||
except Exception:
|
||||
VERSION = "0.0.0"
|
||||
|
||||
# Force 0.0.0.0 to ensure the Windows Host bridge works for Project Nexus
|
||||
# Loopback by default -- the REST APIs are unauthenticated, so 0.0.0.0 exposed
|
||||
# the whole admin+data plane to the LAN. Same knob management/ncp.py uses.
|
||||
BIND_HOST = os.environ.get("NEXUS_BIND_HOST", "127.0.0.1")
|
||||
FRONTEND_DIR = PROJECT_ROOT / "interface" / "web"
|
||||
def _find_npm():
|
||||
"""Prefer nvm's newest node. When the panel is launched from the desktop
|
||||
@@ -51,28 +52,15 @@ BACKEND_CMD = [
|
||||
str(PROJECT_ROOT / "Promethean" / "bin" / "python"),
|
||||
"-m", "uvicorn",
|
||||
"synapse.main:sio_app",
|
||||
"--host", "0.0.0.0",
|
||||
"--host", BIND_HOST,
|
||||
"--port", "8000",
|
||||
"--reload",
|
||||
"--reload-dir", str(PROJECT_ROOT / "synapse"),
|
||||
"--log-level", "info"
|
||||
]
|
||||
|
||||
MEMORY_DIR = PROJECT_ROOT
|
||||
MEMORY_CMD = [
|
||||
str(PROJECT_ROOT / "Promethean" / "bin" / "python"),
|
||||
"-m", "uvicorn",
|
||||
"synapse.memory.service:app",
|
||||
"--host", "0.0.0.0",
|
||||
"--port", "8001",
|
||||
"--reload",
|
||||
"--reload-dir", str(PROJECT_ROOT / "synapse"),
|
||||
"--log-level", "info"
|
||||
]
|
||||
|
||||
FRONTEND_PID = PROJECT_ROOT / "runtime" / "pids" / "frontend.pid"
|
||||
BACKEND_PID = PROJECT_ROOT / "runtime" / "pids" / "backend.pid"
|
||||
MEMORY_PID = PROJECT_ROOT / "runtime" / "pids" / "memory.pid"
|
||||
CHAT_LOG = PROJECT_ROOT / "runtime" / "logs" / "chat.log"
|
||||
|
||||
class NexusControlPanel:
|
||||
@@ -109,10 +97,8 @@ class NexusControlPanel:
|
||||
|
||||
self.frontend_process = None
|
||||
self.backend_process = None
|
||||
self.memory_process = None
|
||||
self._frontend_starting = False
|
||||
self._backend_starting = False
|
||||
self._memory_starting = False
|
||||
self._closing = False
|
||||
|
||||
self._setup_custom_titlebar()
|
||||
@@ -132,9 +118,6 @@ class NexusControlPanel:
|
||||
if self._is_running(frontend_pid):
|
||||
threading.Thread(target=self._monitor_frontend, daemon=True).start()
|
||||
|
||||
if self._is_running(MEMORY_PID):
|
||||
threading.Thread(target=self._monitor_memory, daemon=True).start()
|
||||
|
||||
threading.Thread(target=self._monitor_synapses, daemon=True).start()
|
||||
|
||||
self.update_master_ui()
|
||||
@@ -167,12 +150,11 @@ class NexusControlPanel:
|
||||
|
||||
|
||||
def _init_log_tags(self):
|
||||
for console in [self.sys_console, self.front_console, self.back_console, self.mind_console, self.mem_console]:
|
||||
for console in [self.sys_console, self.front_console, self.back_console, self.mind_console]:
|
||||
console.tag_config("SYSTEM", foreground="#ff00ea")
|
||||
console.tag_config("FRONTEND", foreground="#61dbfb")
|
||||
console.tag_config("BACKEND", foreground="#ffd43b")
|
||||
console.tag_config("MINDTRACE", foreground="#cc88ff")
|
||||
console.tag_config("MEMORY", foreground="#78e08f")
|
||||
console.tag_config("ERROR", foreground="#ff0000")
|
||||
console.tag_config("GPU_NVIDIA", foreground="#76b900")
|
||||
console.tag_config("GPU_AMD", foreground="#ed1c24")
|
||||
@@ -193,7 +175,7 @@ class NexusControlPanel:
|
||||
self.root.geometry(f"+{self.root.winfo_x() + (event.x - self.x)}+{self.root.winfo_y() + (event.y - self.y)}")
|
||||
|
||||
def _clear_all_logs(self):
|
||||
for console in [self.sys_console, self.front_console, self.back_console, self.mem_console, self.mind_console]:
|
||||
for console in [self.sys_console, self.front_console, self.back_console, self.mind_console]:
|
||||
console.delete("1.0", tk.END)
|
||||
|
||||
def _copy_selection(self, widget):
|
||||
@@ -260,19 +242,13 @@ class NexusControlPanel:
|
||||
self.btn_back_stop = tk.Button(controls, text="OFF", **STOP_STYLE, command=self.stop_backend)
|
||||
self.btn_back_stop.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Memory Service Controls
|
||||
self.btn_mem_start = tk.Button(controls, text="MEMORY", fg="#78e08f", **START_STYLE, command=self.start_memory)
|
||||
self.btn_mem_start.pack(side=tk.LEFT, padx=(8, 2))
|
||||
self.btn_mem_stop = tk.Button(controls, text="OFF", **STOP_STYLE, command=self.stop_memory)
|
||||
self.btn_mem_stop.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Terminal Displays
|
||||
sys_f, self.sys_console = self._create_terminal(self.root, " >> SYSTEM STATUS", height=5, text_color="#ff00ea")
|
||||
sys_f.pack(fill=tk.X, padx=10, pady=(0, 8))
|
||||
|
||||
bottom_f = tk.Frame(self.root, bg="#1e1e1e")
|
||||
bottom_f.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 8))
|
||||
for col in range(4):
|
||||
for col in range(3):
|
||||
bottom_f.columnconfigure(col, weight=1, uniform="term")
|
||||
bottom_f.rowconfigure(0, weight=1)
|
||||
|
||||
@@ -280,10 +256,8 @@ class NexusControlPanel:
|
||||
f_f.grid(row=0, column=0, sticky="nsew", padx=(0, 4))
|
||||
b_f, self.back_console = self._create_terminal(bottom_f, " >> SYNAPSE (UVICORN)", text_color="#ffd43b")
|
||||
b_f.grid(row=0, column=1, sticky="nsew", padx=4)
|
||||
mm_f, self.mem_console = self._create_terminal(bottom_f, " >> MEMORY SERVICE", text_color="#78e08f")
|
||||
mm_f.grid(row=0, column=2, sticky="nsew", padx=4)
|
||||
m_f, self.mind_console = self._create_terminal(bottom_f, " >> MINDTRACE", text_color="#cc88ff")
|
||||
m_f.grid(row=0, column=3, sticky="nsew", padx=(4, 0))
|
||||
m_f.grid(row=0, column=2, sticky="nsew", padx=(4, 0))
|
||||
|
||||
def _safe_after(self, delay, func):
|
||||
"""Schedule a Tk callback without ever raising.
|
||||
@@ -305,7 +279,6 @@ class NexusControlPanel:
|
||||
con = self.sys_console
|
||||
if tag == "FRONTEND": con = self.front_console
|
||||
elif tag == "BACKEND": con = self.back_console
|
||||
elif tag == "MEMORY": con = self.mem_console
|
||||
elif tag == "MINDTRACE": con = self.mind_console
|
||||
con.insert(tk.END, f"[{tag}] {message}\n", tag)
|
||||
con.see(tk.END)
|
||||
@@ -358,16 +331,14 @@ class NexusControlPanel:
|
||||
def update_master_ui(self):
|
||||
if self._closing: # window may be destroyed; stop_* still calls this
|
||||
return
|
||||
f, b, m = self._frontend_is_active(), self._backend_is_active(), self._memory_is_active()
|
||||
self.btn_master_start.config(state=tk.DISABLED if (f and b and m) else tk.NORMAL)
|
||||
self.btn_master_stop.config(state=tk.NORMAL if (f or b or m) else tk.DISABLED)
|
||||
f, b = self._frontend_is_active(), self._backend_is_active()
|
||||
self.btn_master_start.config(state=tk.DISABLED if (f and b) else tk.NORMAL)
|
||||
self.btn_master_stop.config(state=tk.NORMAL if (f or b) else tk.DISABLED)
|
||||
self.btn_front_start.config(state=tk.DISABLED if f else tk.NORMAL, bg="#0b3d0b" if f else "#2d2d2d")
|
||||
self.btn_back_start.config(state=tk.DISABLED if b else tk.NORMAL, bg="#3d3d0b" if b else "#2d2d2d")
|
||||
self.btn_mem_start.config(state=tk.DISABLED if m else tk.NORMAL, bg="#0b2d14" if m else "#2d2d2d")
|
||||
self.btn_front_stop.config(state=tk.NORMAL if f else tk.DISABLED)
|
||||
self.btn_front_view.config(state=tk.NORMAL if f else tk.DISABLED)
|
||||
self.btn_back_stop.config(state=tk.NORMAL if b else tk.DISABLED)
|
||||
self.btn_mem_stop.config(state=tk.NORMAL if m else tk.DISABLED)
|
||||
|
||||
def open_frontend_view(self):
|
||||
webbrowser.open_new_tab(FRONTEND_URL)
|
||||
@@ -426,10 +397,6 @@ class NexusControlPanel:
|
||||
self._frontend_starting = False
|
||||
self.frontend_process = p
|
||||
self._write_pid(FRONTEND_PID, p.pid)
|
||||
elif name == "MEMORY":
|
||||
self._memory_starting = False
|
||||
self.memory_process = p
|
||||
self._write_pid(MEMORY_PID, p.pid)
|
||||
else:
|
||||
self._backend_starting = False
|
||||
self.backend_process = p
|
||||
@@ -472,10 +439,6 @@ class NexusControlPanel:
|
||||
self._frontend_starting = False
|
||||
self.frontend_process = None
|
||||
FRONTEND_PID.unlink(missing_ok=True)
|
||||
elif name == "MEMORY":
|
||||
self._memory_starting = False
|
||||
self.memory_process = None
|
||||
MEMORY_PID.unlink(missing_ok=True)
|
||||
else:
|
||||
self._backend_starting = False
|
||||
self.backend_process = None
|
||||
@@ -576,47 +539,13 @@ class NexusControlPanel:
|
||||
self._terminate_pid_file(BACKEND_PID)
|
||||
self.update_master_ui()
|
||||
|
||||
def start_memory(self):
|
||||
if self._memory_is_active():
|
||||
return
|
||||
self._memory_starting = True
|
||||
self.update_master_ui()
|
||||
self.log("SYSTEM", "Starting MEMORY SERVICE...")
|
||||
threading.Thread(target=self._worker, args=("MEMORY", MEMORY_CMD, MEMORY_DIR), daemon=True).start()
|
||||
|
||||
def stop_memory(self):
|
||||
if self.memory_process:
|
||||
self._terminate_process(self.memory_process)
|
||||
self.memory_process = None
|
||||
elif self._is_running(MEMORY_PID):
|
||||
self._terminate_pid_file(MEMORY_PID)
|
||||
self.update_master_ui()
|
||||
|
||||
def _memory_is_active(self):
|
||||
return self._memory_starting or self.memory_process is not None or self._is_running(MEMORY_PID)
|
||||
|
||||
def _monitor_memory(self):
|
||||
log_path = PROJECT_ROOT / "runtime/memory.log"
|
||||
try:
|
||||
with open(log_path, "r") as f:
|
||||
f.seek(0, os.SEEK_END)
|
||||
while self._is_running(MEMORY_PID):
|
||||
line = f.readline()
|
||||
if line:
|
||||
self.log("MEMORY", line.rstrip())
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
except Exception as e:
|
||||
self.log("ERROR", f"MEMORY monitor fault: {e}")
|
||||
|
||||
def start_all(self):
|
||||
self.start_memory()
|
||||
self.start_frontend()
|
||||
self.start_backend()
|
||||
|
||||
def kill_all(self):
|
||||
self.log("SYSTEM", "FORCE KILL — terminating all Nexus processes by port...")
|
||||
ports = {8000: "SYNAPSE", 8001: "MEMORY", 5173: "INTERFACE"}
|
||||
ports = {8000: "SYNAPSE", 5173: "INTERFACE"}
|
||||
for port, name in ports.items():
|
||||
try:
|
||||
r = subprocess.run(["fuser", "-k", f"{port}/tcp"], capture_output=True, timeout=5)
|
||||
@@ -628,21 +557,18 @@ class NexusControlPanel:
|
||||
subprocess.run(["pkill", "-9", "-f", pat], capture_output=True, timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
for pid_file in [FRONTEND_PID, BACKEND_PID, MEMORY_PID]:
|
||||
for pid_file in [FRONTEND_PID, BACKEND_PID]:
|
||||
pid_file.unlink(missing_ok=True)
|
||||
self.frontend_process = None
|
||||
self.backend_process = None
|
||||
self.memory_process = None
|
||||
self._frontend_starting = False
|
||||
self._backend_starting = False
|
||||
self._memory_starting = False
|
||||
self._safe_after(0, self.update_master_ui)
|
||||
self.log("SYSTEM", "Force kill complete.")
|
||||
|
||||
def stop_all(self):
|
||||
self.stop_frontend()
|
||||
self.stop_backend()
|
||||
self.stop_memory()
|
||||
|
||||
def on_close(self):
|
||||
if self._closing:
|
||||
@@ -675,13 +601,13 @@ class NexusControlPanel:
|
||||
except Exception:
|
||||
pass
|
||||
# Services the panel spawned itself (each in its own setsid group):
|
||||
for proc in (self.frontend_process, self.backend_process, self.memory_process):
|
||||
for proc in (self.frontend_process, self.backend_process):
|
||||
if proc:
|
||||
self._terminate_process(proc)
|
||||
# Services attached via pid file (ncp-started, one shared group);
|
||||
# _terminate_pid_file verifies ownership and unlinks. Guarantee the
|
||||
# unlink even for a pid that's already dead so no stale files remain.
|
||||
for pf in (FRONTEND_PID, BACKEND_PID, MEMORY_PID):
|
||||
for pf in (FRONTEND_PID, BACKEND_PID):
|
||||
if self._is_running(pf):
|
||||
self._terminate_pid_file(pf)
|
||||
else:
|
||||
|
||||
+54
-38
@@ -25,6 +25,7 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -97,12 +98,20 @@ def _psutil():
|
||||
|
||||
# -- services ------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Service:
|
||||
def __init__(self, key, label, port, cwd, patterns, argv=None):
|
||||
self.key, self.label, self.port = key, label, port
|
||||
self.cwd, self.patterns, self._argv = cwd, patterns, argv
|
||||
self.pid_file = PID_DIR / f"{key}.pid"
|
||||
self.log_file = LOG_DIR / f"{key}.log"
|
||||
key: str
|
||||
label: str
|
||||
port: int
|
||||
cwd: Path
|
||||
patterns: list
|
||||
_argv: object = None
|
||||
pid_file: Path = field(init=False)
|
||||
log_file: Path = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
self.pid_file = PID_DIR / f"{self.key}.pid"
|
||||
self.log_file = LOG_DIR / f"{self.key}.log"
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
@@ -112,7 +121,7 @@ class Service:
|
||||
return self._argv() if callable(self._argv) else self._argv
|
||||
|
||||
|
||||
# Bind loopback by default: the backend/memory REST APIs are unauthenticated, so
|
||||
# Bind loopback by default: the backend REST API is unauthenticated, so
|
||||
# binding 0.0.0.0 handed the full admin+data plane to any host on the LAN. Set
|
||||
# NEXUS_BIND_HOST=0.0.0.0 to opt into LAN exposure once real auth is in place.
|
||||
BIND_HOST = os.environ.get("NEXUS_BIND_HOST", "127.0.0.1")
|
||||
@@ -131,9 +140,6 @@ def _uvicorn(app: str, port: int):
|
||||
|
||||
|
||||
SERVICES = {
|
||||
"memory": Service("memory", "NEXUS MEMORY SERVICE", 8001, ROOT,
|
||||
["uvicorn synapse.memory"],
|
||||
lambda: _uvicorn("synapse.memory.service:app", 8001)),
|
||||
"backend": Service("backend", "NEXUS BACKEND SERVICE", 8000, ROOT,
|
||||
["uvicorn synapse.main"],
|
||||
lambda: _uvicorn("synapse.main:sio_app", 8000)),
|
||||
@@ -218,10 +224,10 @@ _POLL_STEP = 0.25
|
||||
|
||||
def wait_for_port(svc: Service, timeout: int = 30) -> bool:
|
||||
if http_ok(svc.url):
|
||||
# "READY", not "already running": `ncp start` launches memory and backend
|
||||
# together and only then waits on each, so by the time the backend's turn
|
||||
# comes it is normally up - and reporting "already running" for a service
|
||||
# this same command started two seconds ago reads like a stale process.
|
||||
# "READY", not "already running": `ncp start` launches its services and
|
||||
# only then waits on each, so by the time a service's turn comes it is
|
||||
# normally up - and reporting "already running" for a service this same
|
||||
# command started two seconds ago reads like a stale process.
|
||||
print(f" {svc.label} READY (:{svc.port})")
|
||||
return True
|
||||
for _ in range(int(timeout / _POLL_STEP)):
|
||||
@@ -357,9 +363,7 @@ def stop_ollama() -> None:
|
||||
# -- commands ------------------------------------------------------------------
|
||||
|
||||
def cmd_start(target) -> None:
|
||||
if target in ("--memory", "-m"):
|
||||
launch(SERVICES["memory"]); wait_for_port(SERVICES["memory"])
|
||||
elif target in ("--backend", "-b"):
|
||||
if target in ("--backend", "-b"):
|
||||
launch(SERVICES["backend"]); wait_for_port(SERVICES["backend"])
|
||||
start_ollama()
|
||||
elif target in ("--frontend", "-f"):
|
||||
@@ -381,9 +385,7 @@ def cmd_start(target) -> None:
|
||||
# and falls through to the ~30s force-kill path). Still available on
|
||||
# demand via `ncp start --frontend`.
|
||||
t0 = time.perf_counter()
|
||||
launch(SERVICES["memory"])
|
||||
launch(SERVICES["backend"])
|
||||
wait_for_port(SERVICES["memory"])
|
||||
wait_for_port(SERVICES["backend"])
|
||||
t_services = time.perf_counter()
|
||||
start_ollama(background=True)
|
||||
@@ -391,7 +393,7 @@ def cmd_start(target) -> None:
|
||||
launch(SERVICES["frontend"])
|
||||
t_bg = time.perf_counter()
|
||||
print("\nBoot timing:")
|
||||
print(f" services (memory+backend) : {t_services - t0:5.1f}s")
|
||||
print(f" backend : {t_services - t0:5.1f}s")
|
||||
print(f" ollama + frontend (bg kickoff) : {t_bg - t_services:5.1f}s")
|
||||
print(f" total to interactive : {t_bg - t0:5.1f}s")
|
||||
else:
|
||||
@@ -399,14 +401,11 @@ def cmd_start(target) -> None:
|
||||
|
||||
|
||||
def cmd_stop(target) -> None:
|
||||
if target in ("--memory", "-m"):
|
||||
stop_service(SERVICES["memory"])
|
||||
elif target in ("--backend", "-b"):
|
||||
if target in ("--backend", "-b"):
|
||||
stop_ollama(); stop_service(SERVICES["backend"])
|
||||
elif target in ("--frontend", "-f"):
|
||||
stop_service(SERVICES["frontend"])
|
||||
elif target in (None, "", "all"):
|
||||
stop_service(SERVICES["memory"])
|
||||
stop_ollama()
|
||||
stop_service(SERVICES["backend"])
|
||||
stop_service(SERVICES["frontend"])
|
||||
@@ -416,7 +415,7 @@ def cmd_stop(target) -> None:
|
||||
|
||||
def cmd_kill() -> None:
|
||||
print("Force-killing all Nexus processes...")
|
||||
for port, name in ((8000, "SYNAPSE"), (8001, "MEMORY"),
|
||||
for port, name in ((8000, "SYNAPSE"),
|
||||
(5173, "INTERFACE"), (11434, "OLLAMA")):
|
||||
if kill_port(port):
|
||||
print(f" KILLED: {name} (:{port})")
|
||||
@@ -443,7 +442,6 @@ def cmd_status() -> None:
|
||||
|
||||
print("Backend:")
|
||||
one("Synapse ", SERVICES["backend"])
|
||||
one("Memory service", SERVICES["memory"])
|
||||
print("\nFrontend:")
|
||||
one("Vite ", SERVICES["frontend"])
|
||||
print("\nModel server:")
|
||||
@@ -459,26 +457,25 @@ def _tail(path: Path, n: int) -> None:
|
||||
print(f" (no log at {path})")
|
||||
|
||||
|
||||
LOG_HEADINGS = {"memory": "MEMORY SERVICE", "backend": "BACKEND", "frontend": "FRONTEND"}
|
||||
LOG_HEADINGS = {"backend": "BACKEND", "frontend": "FRONTEND"}
|
||||
|
||||
|
||||
def cmd_logs(target) -> None:
|
||||
named = {"--frontend": "frontend", "-f": "frontend",
|
||||
"--backend": "backend", "-b": "backend",
|
||||
"--memory": "memory", "-m": "memory"}
|
||||
"--backend": "backend", "-b": "backend"}
|
||||
if target in named:
|
||||
key = named[target]
|
||||
print(f"=== {LOG_HEADINGS[key]} LOGS ===")
|
||||
_tail(SERVICES[key].log_file, 50)
|
||||
elif target in (None, "", "all"):
|
||||
for i, key in enumerate(("memory", "backend", "frontend")):
|
||||
for i, key in enumerate(("backend", "frontend")):
|
||||
if i:
|
||||
print()
|
||||
print(f"=== {LOG_HEADINGS[key]} LOGS ===")
|
||||
_tail(SERVICES[key].log_file, 30)
|
||||
else:
|
||||
print(f"Unknown logs target: '{target}'")
|
||||
print("Usage: ncp logs [frontend|backend|memory|all]")
|
||||
print("Usage: ncp logs [frontend|backend|all]")
|
||||
|
||||
|
||||
def _apply_fixes() -> None:
|
||||
@@ -586,11 +583,11 @@ def cmd_doctor(fix: bool = False) -> None:
|
||||
mark(importable("from synapse.main import sio_app"),
|
||||
"Backend module importable", "Backend module failed to import")
|
||||
|
||||
print("\nChecking memory service...")
|
||||
print("\nChecking memory...")
|
||||
mark((ROOT / "synapse" / "memory").is_dir(),
|
||||
"Memory module directory found", "Missing memory module directory")
|
||||
mark(importable("from synapse.memory.service import app"),
|
||||
"Memory service module importable", "Memory service module failed to import")
|
||||
mark(importable("from synapse.memory.curator import extract_for_conversation"),
|
||||
"Memory curator importable", "Memory curator failed to import")
|
||||
mark(os.access(ROOT / "synapse" / "memory", os.W_OK),
|
||||
"Memory database directory writable", "Memory database directory not writable")
|
||||
|
||||
@@ -631,6 +628,26 @@ def cmd_update() -> None:
|
||||
cmd_doctor()
|
||||
|
||||
|
||||
def cmd_upgrade() -> int:
|
||||
"""Pull + rebuild + restart the backend. This is what the web UI's "install
|
||||
update" button runs, so it must be spawned DETACHED from the backend - it
|
||||
stops the very server that asked for it.
|
||||
|
||||
Ollama is deliberately left alone (stop_service, not cmd_stop): it is a
|
||||
separate process on :11434, the restore does not touch a present binary, and
|
||||
reloading a multi-GB model is the slowest part of a restart.
|
||||
"""
|
||||
print("Stopping backend...")
|
||||
stop_service(SERVICES["backend"])
|
||||
# --no-desktop: an in-app update should not rewrite XFCE panels/theme.
|
||||
rc = sync_py("restore", "--no-desktop")
|
||||
print("\nRestarting backend...")
|
||||
launch(SERVICES["backend"])
|
||||
ok = wait_for_port(SERVICES["backend"])
|
||||
print("Backend is back up." if ok else "Backend did not come back - see runtime/backend.log")
|
||||
return rc if ok else 1
|
||||
|
||||
|
||||
def cmd_clean() -> None:
|
||||
print("Cleaning Nexus runtime files...\n")
|
||||
print("Removing PID files...")
|
||||
@@ -731,7 +748,6 @@ def cmd_web() -> int:
|
||||
started. Windows uses the pywebview window launch_nexus.ps1 opens."""
|
||||
if WINDOWS:
|
||||
if not http_ok(SERVICES["backend"].url):
|
||||
launch(SERVICES["memory"])
|
||||
launch(SERVICES["backend"])
|
||||
wait_for_port(SERVICES["backend"])
|
||||
return subprocess.run([str(PYTHON), str(ROOT / "bin" / "nexus_window.py")]).returncode
|
||||
@@ -755,14 +771,12 @@ Commands:
|
||||
show <id> Print a playbook's goal + instructions
|
||||
history [query] Recent conversations (optional keyword)
|
||||
|
||||
start Start ALL Nexus services (memory + backend + frontend)
|
||||
--memory, -m Start only the memory service
|
||||
start Start ALL Nexus services (backend + frontend)
|
||||
--frontend,-f Start only the frontend
|
||||
--backend, -b Start only the backend
|
||||
--ai, -a Start only the AI (Ollama); `start`/`start -b` already include it
|
||||
|
||||
stop Stop ALL Nexus services
|
||||
--memory, -m Stop only the memory service
|
||||
--frontend,-f Stop only the frontend
|
||||
--backend, -b Stop only the backend
|
||||
|
||||
@@ -771,12 +785,12 @@ Commands:
|
||||
|
||||
status Show service status
|
||||
logs Show logs for all services
|
||||
--memory, -m Memory service logs
|
||||
--frontend,-f Frontend logs
|
||||
--backend, -b Backend logs
|
||||
|
||||
doctor [--fix] Run Nexus diagnostics (--fix applies safe repairs)
|
||||
update Update Nexus dependencies
|
||||
upgrade Pull, rebuild and restart the backend (what the UI's update button runs)
|
||||
clean Remove runtime files and caches
|
||||
|
||||
models
|
||||
@@ -824,6 +838,8 @@ def main(argv) -> int:
|
||||
cmd_doctor(fix="--fix" in rest)
|
||||
elif cmd == "update":
|
||||
cmd_update()
|
||||
elif cmd == "upgrade":
|
||||
return cmd_upgrade()
|
||||
elif cmd == "clean":
|
||||
cmd_clean()
|
||||
elif cmd == "nvidia-reqs":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launch NexusOS as a standalone Edge app window. Single-process mode: the
|
||||
# backend on :8000 serves the built web UI itself (no separate Vite), so this
|
||||
# only starts the memory service + backend. The AI (Ollama) does NOT auto-start
|
||||
# only starts the backend. The AI (Ollama) does NOT auto-start
|
||||
# — turn it on from the UI's Start AI button. Closing the app window shuts the
|
||||
# services back down (matches the launcher's "closing the window shuts it down").
|
||||
NEXUS_ROOT="$(cd "$(dirname "$(realpath "$0")")/.." && pwd)"
|
||||
@@ -22,7 +22,7 @@ if curl -s --max-time 1 "$APP_URL/" >/dev/null 2>&1; then
|
||||
OWN_SERVICES=0
|
||||
else
|
||||
OWN_SERVICES=1
|
||||
# Single-process app: memory + backend only. Backend serves the built UI at
|
||||
# Single-process app: backend only. Backend serves the built UI at
|
||||
# :8000; no frontend/Vite process, no AI auto-start.
|
||||
"$NEXUS_ROOT/management/nexus-cli.sh" start -m
|
||||
"$NEXUS_ROOT/management/nexus-cli.sh" start -b
|
||||
|
||||
Reference in New Issue
Block a user