Merge origin/main (v1.2.0: Projects, modules, in-app updates)

Reconciles 17 commits of this session's work (self-alteration tools,
vendored Curry, slash-command dispatch, Windows toolchain/gate fixes)
against origin/main's v1.2.0 sync (Projects/RAG scoping, a new modules/
system for mail and network, in-app updates, the standalone memory
microservice folded into an in-process curator, KDE desktop theme
overhaul). Nine real conflicts, each resolved by hand after reading both
sides' actual diffs rather than picking one side wholesale:

- synapse/tools.py, tests/test_tools.py: origin/main's diff here was
  small and clean (read_file/list_files, two new tests) despite git's
  diff3 flagging the whole file as one conflict blob -- reset to this
  branch's version and hand-spliced their addition in at the same
  points they used, rather than trying to reconcile a false 800-line
  conflict. Found and fixed a real bug while verifying: _list_files
  returned backslash-separated paths on Windows, which don't match the
  forward-slash glob patterns the tool's own schema documents.
- synapse/main.py: kept this branch's cue-based standing advertisement
  of render_preview/run_snippet (independent of any playbook granting
  them) AND adopted origin/main's fix for routed reference playbooks
  not bringing their own tools along -- dropping either would have been
  a real regression, not just a style difference. Also: the standalone
  memory service (port 8001) is gone upstream, so its dead CORS/kill-
  target entries were removed; NEXUS_BACKEND_PORT parameterization and
  the manage_ollama-conditional kill logic (this branch's remote-Ollama
  support) were kept over origin/main's hardcoded equivalents.
- synapse/memory/store.py: kept this branch's _delete_message_vectors
  helper (already reused elsewhere, batches to stay under SQLite's
  variable limit) over origin/main's inline duplicate of the same fix.
- synapse/nexus_config.py, nexusos_cli/ncp.py: dropped the now-dead
  memory-service port/service entries; kept NEXUS_BACKEND_PORT env
  override and the manage_ollama-conditional kill-target list.
- CLAUDE.md, README.md: merged both sides' additions, no real conflict.

Found and fixed three more issues while independently verifying the
merged tree, none of them mine or origin/main's alone -- only visible
once both sides actually ran together:

- modules/ (the new mail+network package) was never added to
  pyproject.toml's wheel `packages` list OR the sdist's `include`
  allowlist, so `from modules.registry import ROUTERS` in main.py would
  ImportError on any wheel install. Fixed both; bin/check.sh's
  packaging gate now asserts modules/ actually ships. tests/
  test_packaging_deps.py's FIRST_PARTY/SHIPPED_PACKAGES sets were
  updated to recognize the new package.
- tests/test_mail_creds.py's 0600-mode assertions are POSIX-only --
  NTFS has no equivalent permission bits, so os.open(path, 0o600) on
  Windows just creates a normal file and stat.S_IMODE reports 0o666
  regardless. Made the assertions platform-aware rather than skip real
  coverage (the temp-file-cleanup and password round-trip checks in the
  same test still run on Windows) or paper over a genuine OS
  limitation with a fake pass.
- tests/test_kde_theme.py used bare Path.read_text() in fifteen places;
  Windows' default locale encoding (cp1252, not UTF-8) can't decode a
  real UTF-8 byte in the QML it reads, and did fail on one of the
  fifteen. Fixed all fifteen, not just the one that happened to trip
  today, since the other fourteen were equally fragile.

Verified: full bin/check.sh reports OK end-to-end on this Windows
checkout -- pytest (tests + management): 295 passed, 0 failed, 9
skipped; eslint clean; frontend node:test 57/57; PowerShell/shell
parse clean; wheel + sdist pass twine check and now correctly carry
modules/ (60 files, up from 52 pre-merge). synapse.main:app builds
with 74 routes (up from 54 pre-merge, matching the new Projects/mail/
network endpoints).
This commit is contained in:
2026-08-26 02:09:23 -05:00
91 changed files with 4293 additions and 1896 deletions
+12
View File
@@ -51,6 +51,18 @@ if [ -d "$HOME/.config/plank" ]; then
mkdir -p "$snap/plank"
cp -rf "$HOME/.config/plank/." "$snap/plank/" 2>/dev/null || true
fi
# The .dockitem files alone are NOT the dock. Everything else — item list, theme,
# icon size, position, hide mode — lives in dconf, and on startup Plank DELETES
# every .dockitem not in its list, so copying files alone restores nothing.
# `monitor` is stripped: it's per-machine, and plank-primary-watch.sh sets it.
dconf dump /net/launchpad/plank/ 2>/dev/null | grep -v '^monitor=' > "$snap/plank-dconf.ini" || true
# Dock launchers point at .desktop files in ~/.local/share/applications, which
# is outside the repo — Plank prunes any item whose target is missing.
mkdir -p "$snap/applications"
for d in $(sed -n 's#.*applications/\([^.]*\.desktop\).*#\1#p' \
"$snap"/plank/dock1/launchers/*.dockitem 2>/dev/null); do
cp -f "$HOME/.local/share/applications/$d" "$snap/applications/$d" 2>/dev/null || true
done
# Copy the Claude memory notes (machine knowledge under ~/.claude) into the
# repo so they're versioned too. Some aren't Nexus-specific (Fusion 360, etc.).
+2
View File
@@ -81,6 +81,8 @@ if "synapse/curry_core.py" not in names or "synapse/curry_store.py" not in names
sys.exit("wheel is missing vendored Curry (synapse/curry_core.py / curry_store.py)")
if "synapse/slash_commands.py" not in names:
sys.exit("wheel is missing synapse/slash_commands.py")
if not any(n.startswith("modules/") for n in names):
sys.exit("wheel is missing the modules/ package (mail, network) - check pyproject.toml packages=[...]")
print(f"wheel OK: {len(names)} files")
PY
else
+7 -16
View File
@@ -10,7 +10,7 @@ The window opens IMMEDIATELY on a loading page and navigates to the app once the
backend answers, rather than waiting up to 40s with nothing on screen and then
appearing. One window the whole time: nothing pops up and vanishes, and there is
no moment where the user is left wondering whether the command did anything.
The loading page's status line is updated live (memory -> backend -> ready) via
The loading page's status line is updated live (backend -> ready) via
evaluate_js, instead of sitting on one static sentence for the whole wait - the
point is a user watching the window can tell what stage it's stuck on, without
needing to go read a terminal or a log file to find out.
@@ -24,7 +24,6 @@ import time
import urllib.request
URL = "http://localhost:8000"
MEMORY_URL = "http://localhost:8001/"
ICON_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "NexusOS.ico")
# Inline so it renders with no server and no asset files - the whole point is
@@ -47,7 +46,7 @@ LOADING_HTML = """<!doctype html>
<div class="box">
<div class="ring"></div>
<h1>Starting NexusOS</h1>
<p id="status">Starting memory service...</p>
<p id="status">Starting backend...</p>
</div>
</body></html>"""
@@ -56,7 +55,7 @@ FAILED_HTML = LOADING_HTML.replace(
).replace(
"Starting NexusOS", "Backend did not start"
).replace(
'id="status">Starting memory service...',
'id="status">Starting backend...',
'id="status">Nothing answered on :8000 after 40s. Check: ncp logs -b',
)
@@ -80,23 +79,15 @@ def _http_ok(url: str, timeout: float = 2.0) -> bool:
def _wait_for_backend(win, timeout: float = 40.0) -> bool:
"""Poll memory, then the backend, updating the on-screen status as each
comes up - so the window never loads before the server is up (which shows
a localhost error the webview won't retry), and never sits on one sentence
while actually moving through two separate services."""
"""Poll the backend, updating the on-screen status as it comes up - so the
window never loads before the server is up, which shows a localhost error
the webview won't retry."""
status_url = URL.rstrip("/") + "/status"
deadline = time.time() + timeout
memory_ready = _http_ok(MEMORY_URL)
if memory_ready:
_set_status(win, "Starting backend...")
_set_status(win, "Starting backend...")
while time.time() < deadline:
if not memory_ready:
memory_ready = _http_ok(MEMORY_URL)
if memory_ready:
_set_status(win, "Starting backend...")
try:
# 5s, not 2: /status probes Ollama, and a wedged Ollama made it
# slower than a 2s ceiling - the backend was up and answering 200
+16
View File
@@ -36,6 +36,22 @@ cp -f "$NEXUS/management/autostart/nm-applet.desktop" "$AUTOSTART/nm-appl
cp -f "$NEXUS/management/autostart/blueman.desktop" "$AUTOSTART/blueman.desktop"
cp -f "$NEXUS/management/autostart/blueman-applet.desktop" "$AUTOSTART/blueman-applet.desktop"
# Suppress the XApp tray icons (Update Manager, System Reports, nvidia-prime).
# XApp.StatusIcon only speaks its own org.x.StatusIcon protocol; with no XApp
# status monitor on Plasma it falls back to a legacy XEmbed icon, and
# xembedsniproxy renders those as solid black silhouettes. Plasma's own
# DiscoverNotifier already covers update notifications, and nvidia-prime's
# GPU-mux switching is reachable from `prime-select`. Delete the override to
# get one back -- it will still render black.
for app in mintupdate mintreport nvidia-prime; do
printf '[Desktop Entry]\nType=Application\nName=%s\nExec=/bin/true\nHidden=true\nX-XFCE-Autostart-Override=true\n' \
"$app" > "$AUTOSTART/$app.desktop"
done
# Plank runs under the primary-follow watcher, which also restarts it if it dies.
# Nothing else launches Plank — a restored ~/.config/plank only holds its
# launchers, so without this the dock is simply absent on a fresh box.
ln -sf "$NEXUS/management/autostart/plank.desktop" "$AUTOSTART/plank.desktop"
# ── Register the genmon applets into the XFCE panel ────────────────────────
# The network applet's genmon (plugin-13) was added by hand once; the Nexus
+2 -5
View File
@@ -15,15 +15,13 @@ port_up() {
state() { port_up "$1" && echo "up" || echo "down"; }
BACKEND=$(state 8000) # Synapse
MEMORY=$(state 8001) # Memory service
FRONTEND=$(state 5173) # Vite frontend
up=0
[ "$BACKEND" = up ] && up=$((up + 1))
[ "$MEMORY" = up ] && up=$((up + 1))
[ "$FRONTEND" = up ] && up=$((up + 1))
if [ "$up" -eq 3 ]; then ICON="$ICON_DIR/nexus-on.png"
if [ "$up" -eq 2 ]; then ICON="$ICON_DIR/nexus-on.png"
elif [ "$up" -eq 0 ]; then ICON="$ICON_DIR/nexus-off.png"
else ICON="$ICON_DIR/nexus-partial.png"
fi
@@ -31,8 +29,7 @@ fi
dot() { [ "$1" = up ] && echo "●" || echo "○"; }
echo "<img>$ICON</img>"
echo "<tool>NexusOS — ${up}/3 services up
echo "<tool>NexusOS — ${up}/2 services up
$(dot "$BACKEND") Synapse backend :8000
$(dot "$MEMORY") Memory service :8001
$(dot "$FRONTEND") Frontend (Vite) :5173</tool>"
echo "<click>$HOME/.local/bin/nexus-popup.py</click>"
-1
View File
@@ -34,7 +34,6 @@ APP_URL = "http://localhost:5173"
# name, port, nexus-cli flag
SERVICES = [
("Synapse backend", 8000, "--backend"),
("Memory service", 8001, "--memory"),
("Frontend (Vite)", 5173, "--frontend"),
]
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Installs Promethean Terminal: downloads kitty, wires up the config and icon,
# and registers the desktop entry in the app menu and on the Desktop.
set -euo pipefail
# This script lives at <repo>/bin/promethean/install.sh; its own dir holds the
# kitty conf + desktop entry it installs.
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
KITTY_BIN="$HOME/.local/kitty.app/bin/kitty"
DESKTOP_FILE="$SELF_DIR/promethean-terminal.desktop"
CONF="$SELF_DIR/kitty.conf"
NEXUS_ROOT="$(cd "$SELF_DIR/../.." && pwd)"
RCFILE="$NEXUS_ROOT/.promethean_bashrc"
ICON="$NEXUS_ROOT/assets/promethean-terminal.png"
# ── 1. Download kitty ────────────────────────────────────────────────────────
if [[ -x "$KITTY_BIN" ]]; then
echo "kitty already at $KITTY_BIN — skipping download (run with --update to force)"
if [[ "${1:-}" == "--update" ]]; then
echo "→ Updating kitty..."
curl -fsSL https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin launch=n
fi
else
echo "→ Downloading kitty to ~/.local/kitty.app/ ..."
curl -fsSL https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin launch=n
fi
echo " kitty $("$KITTY_BIN" --version)"
# ── 2. Ensure .promethean_bashrc exists ──────────────────────────────────────
if [[ ! -f "$RCFILE" ]]; then
echo "→ Creating $RCFILE (sourcing ~/.bashrc) ..."
mkdir -p "$(dirname "$RCFILE")"
cat > "$RCFILE" <<'EOF'
# Promethean Terminal shell init
[[ -f ~/.bashrc ]] && source ~/.bashrc
export VIRTUAL_ENV_DISABLE_PROMPT=1
if [[ -f "$HOME/nexus-core/Promethean/bin/activate" ]]; then
source "$HOME/nexus-core/Promethean/bin/activate"
fi
export PS1='\[\e[0;35m\](Promethean)\[\e[0m\] ${debian_chroot:+($debian_chroot)}\[\e[0;37m\]\u@\h\[\e[0m\]:\[\e[1;32m\]\w\[\e[0m\]\$ '
printf '\e]0;Promethean Terminal\a'
EOF
fi
# ── 3. Write the .desktop file (repo copy + installed copies) ────────────────
write_desktop() {
local dest="$1"
cat > "$dest" <<EOF
[Desktop Entry]
Type=Application
Name=Promethean Terminal
Comment=Launch a terminal with the Promethean environment
Exec=$KITTY_BIN --config $CONF --class promethean-terminal -e bash --rcfile $RCFILE -i
Icon=$ICON
Terminal=false
Categories=Utility;TerminalEmulator;
StartupNotify=false
StartupWMClass=promethean-terminal
EOF
}
echo "→ Writing desktop entry..."
write_desktop "$DESKTOP_FILE"
write_desktop "$HOME/.local/share/applications/promethean-terminal.desktop"
# ── 4. Desktop icon ──────────────────────────────────────────────────────────
DESKTOP_DIR="$HOME/Desktop"
mkdir -p "$DESKTOP_DIR"
cp "$DESKTOP_FILE" "$DESKTOP_DIR/promethean-terminal.desktop"
chmod +x "$DESKTOP_DIR/promethean-terminal.desktop"
# Mark trusted so XFCE/Nemo doesn't show the "untrusted" banner
gio set "$DESKTOP_DIR/promethean-terminal.desktop" metadata::trusted true 2>/dev/null \
|| xfce4-file-manager --quit 2>/dev/null || true
# ── 5. Refresh app menu ──────────────────────────────────────────────────────
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
echo ""
echo "Done. Promethean Terminal is installed."
echo " Binary : $KITTY_BIN"
echo " Config : $CONF"
echo " Icon : $ICON"
echo " Desktop: $DESKTOP_DIR/promethean-terminal.desktop"
+23
View File
@@ -0,0 +1,23 @@
# Promethean Terminal — Project Nexus
# Loaded via: kitty --config /home/jon/nexus-core/bin/promethean/kitty.conf
# Window title — disable kitty's shell integration title override
# so the PS1 escape (\e]0;Promethean Terminal\a) controls the title
shell_integration enabled no-title
# Single-instance mode: subsequent launches reuse the running process (instant open)
allow_remote_control yes
single_instance yes
# Colors
background #1a0030
foreground #cccccc
cursor #b040c0
cursor_text_color #0d0010
# Selection
selection_background #3d0060
selection_foreground #ffffff
# Tab bar (hidden — single window use)
tab_bar_style hidden
+10
View File
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=Promethean Terminal
Comment=Launch a terminal with the Promethean environment
Exec=/home/jon/.local/kitty.app/bin/kitty --config /home/jon/nexus-core/bin/promethean/kitty.conf --class promethean-terminal -e bash --rcfile /home/jon/nexus-core/.promethean_bashrc -i
Icon=/home/jon/nexus-core/assets/promethean-terminal.png
Terminal=false
Categories=Utility;TerminalEmulator;
StartupNotify=false
StartupWMClass=promethean-terminal
+48 -12
View File
@@ -1,17 +1,20 @@
#!/bin/bash
# The Linux-only half of a restore: system packages, the bundled Ollama binary,
# and the XFCE desktop wiring (panel, wallpaper, theme, terminal, branding).
# and the desktop wiring (panel, wallpaper, theme, terminal, branding) for
# whichever of XFCE / Plasma is installed.
# None of it means anything on Windows, which is why it lives here instead of in
# bin/sync.py — sync.py owns the portable half and calls this in three stages:
#
# restore-linux.sh prep Before the rebuild: system packages the build needs.
# restore-linux.sh runtime After: Ollama binary + shell wiring. Any Linux box
# needs these to actually run NexusOS.
# restore-linux.sh desktop After: the XFCE look (panel, wallpaper, theme,
# branding). Cosmetic, this box's setup, and the only
# stage that rewrites files outside the repo and $HOME.
# Skippable with `--no-desktop`; auto-skipped off XFCE.
# restore-linux.sh desktop After: the desktop look (Plasma global theme,
# XFCE panel/wallpaper/theme, boot + distro
# branding). Cosmetic, this box's setup, and the
# only stage that rewrites files outside the repo
# and $HOME. Skippable with `--no-desktop`; each
# desktop's branch auto-skips when absent.
#
# Not meant to be run by hand — use `ncp restore` (python bin/sync.py restore).
@@ -123,15 +126,28 @@ EOF
exit 0
fi
# --- desktop stage: XFCE only, and it writes outside the repo -----------------
# Everything below reconfigures the logged-in user's desktop. On any other DE it
# would be actively wrong (copying XFCE channel XMLs onto a GNOME box does
# nothing good), so bail rather than make a mess of someone else's machine.
if ! command -v xfconf-query >/dev/null; then
echo "Not an XFCE session (no xfconf-query) — skipping desktop wiring."
exit 0
# --- desktop stage: reconfigures the logged-in user's desktop ------------------
# Keyed on what is installed, not on one either/or: XFCE and Plasma can both be
# present (this box runs both and picks at the SDDM greeter), and each branch
# writes only to its own config. Anything neither branch matches falls through
# to the boot/distro branding at the bottom, which is DE-independent.
# Plasma. install-plasma.sh is idempotent and applies live when a session is
# running; --no-sddm because bin/boot-branding.sh owns the SDDM theme, and
# letting both write it was how the setting ended up in two files.
if command -v lookandfeeltool >/dev/null && [ -x "$NEXUS_ROOT/assets/themes/KDE/install-plasma.sh" ]; then
echo ""
echo "Restoring NexusOS Plasma theme..."
"$NEXUS_ROOT/assets/themes/KDE/install-plasma.sh" --no-sddm \
|| echo "Warning: Plasma theme restore failed."
fi
# XFCE. Copying XFCE channel XMLs onto a box with no XFCE does nothing good, so
# this whole branch is skipped rather than half-applied.
if ! command -v xfconf-query >/dev/null; then
echo "No XFCE (no xfconf-query) — skipping the XFCE desktop wiring."
else
# Panel layout, wallpaper, compositing, keybindings and terminal profile all live
# in the xfconf channel XMLs — restore them by copying the files back. xfconfd
# caches channels in memory and rewrites them on exit, so it has to die first or
@@ -163,8 +179,26 @@ if [ -f "$NEXUS_ROOT/bin/panel/plank-primary-watch.sh" ]; then
fi
plank_snap="$NEXUS_ROOT/assets/themes/restore-snapshot/plank"
if [ -d "$plank_snap" ]; then
# Plank owns its item list in dconf and deletes any .dockitem missing from it
# on startup, so copying files alone silently restores nothing. Order here:
# kill Plank (a running one prunes what we copy), put the launcher .desktop
# targets back, copy the items, set the list, restart.
pkill -x plank 2>/dev/null && sleep 1
mkdir -p "$HOME/.local/share/applications"
cp -f "$NEXUS_ROOT/assets/themes/restore-snapshot/applications"/*.desktop \
"$HOME/.local/share/applications/" 2>/dev/null || true
mkdir -p "$HOME/.config/plank"
cp -rf "$plank_snap/." "$HOME/.config/plank/" # /. = contents, else it nests
plank_cfg="$NEXUS_ROOT/assets/themes/restore-snapshot/plank-dconf.ini"
if [ -s "$plank_cfg" ]; then
dconf load /net/launchpad/plank/ < "$plank_cfg" 2>/dev/null || true
fi
theme_snap="$NEXUS_ROOT/assets/themes/plank"
if [ -d "$theme_snap" ]; then
mkdir -p "$HOME/.local/share/plank/themes"
cp -rf "$theme_snap/." "$HOME/.local/share/plank/themes/"
fi
command -v plank >/dev/null && setsid plank >/dev/null 2>&1 </dev/null &
fi
echo ""
@@ -196,6 +230,8 @@ else
echo "Warning: $panel_installer not found — skipping panel install."
fi
fi
# Boot branding — the Plymouth splash. Not XFCE-specific, but it's cosmetic and
# writes outside the repo, so it belongs to the skippable stage like os-release
# below. Self-skips when the theme is already installed (initramfs rebuild is slow).