@rubytech/create-realagent 1.0.676 → 1.0.678
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/dist/index.js +77 -166
- package/dist/uninstall.js +24 -0
- package/package.json +1 -1
- package/payload/platform/plugins/docs/references/deployment.md +2 -3
- package/payload/platform/plugins/docs/references/internals.md +4 -0
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +2 -2
- package/payload/platform/scripts/vnc.sh +34 -13
- package/payload/platform/templates/systemd/maxy-edge.service +33 -0
- package/payload/server/maxy-edge.js +2 -0
- package/payload/server/public/assets/{admin-DQmUdTBa.js → admin-BBL1no_g.js} +1 -1
- package/payload/server/public/assets/{data-DVlvxbTt.js → data-DUSyrydY.js} +1 -1
- package/payload/server/public/assets/{file-OY_hX2wu.js → file-CDJ6dUV3.js} +1 -1
- package/payload/server/public/assets/graph-CWcYp5bE.js +50 -0
- package/payload/server/public/assets/{house-CgENfOCP.js → house-CNP_bwvT.js} +1 -1
- package/payload/server/public/assets/{jsx-runtime-Bu4vXoe7.css → jsx-runtime-BFFQvkdQ.css} +1 -1
- package/payload/server/public/assets/{public-Clp4VPwo.js → public-sHoAccvb.js} +1 -1
- package/payload/server/public/assets/{share-2-RSIR3MmX.js → share-2-DBcb9j6E.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-B0FI_hts.js → useVoiceRecorder-CtSgpc95.js} +1 -1
- package/payload/server/public/assets/{x-DKZ5NR3n.js → x-CTVJaC_u.js} +1 -1
- package/payload/server/public/data.html +6 -6
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +7 -7
- package/payload/server/public/public.html +4 -4
- package/payload/server/server.js +427 -649
- package/payload/platform/templates/dotfiles/.tmux.conf +0 -1
- package/payload/platform/templates/systemd/maxy-ttyd.service +0 -25
- package/payload/server/public/assets/graph-BDaM4Qer.js +0 -49
- /package/payload/server/public/assets/{jsx-runtime-C_VUlXvu.js → jsx-runtime-BVKWELH6.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, readdirSync, appendFileSync, openSync, closeSync, chmodSync, symlinkSync, unlinkSync, lstatSync, readlinkSync, accessSync, constants as fsConstants } from "node:fs";
|
|
4
4
|
import { resolve, join, dirname } from "node:path";
|
|
5
|
-
import { randomBytes
|
|
6
|
-
import { TTYD_VERSION, TTYD_SHA256_BY_ARCH, mapUnameToTtydArch, ttydDownloadUrl, } from "./pinned-binaries.js";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
7
6
|
const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
|
|
8
7
|
// Brand manifest — read from payload to derive all brand-specific installation values.
|
|
9
8
|
// The bundler stamps brand.json into the payload at build time.
|
|
@@ -366,11 +365,7 @@ function installSystemDeps() {
|
|
|
366
365
|
// class where PID is alive but no window is mapped on the target display.
|
|
367
366
|
const VNC_DEPS = ["tigervnc-standalone-server", "python3-websockify", "novnc", "xdg-utils", "chromium", "xterm", "xdotool"];
|
|
368
367
|
const WIFI_DEPS = ["hostapd", "dnsmasq"];
|
|
369
|
-
|
|
370
|
-
// ttyd is NOT in Debian Bookworm's apt repo (Task 602) — it ships as a
|
|
371
|
-
// pinned upstream binary installed by provisionTtydBinary() in step 11.
|
|
372
|
-
const TERMINAL_DEPS = ["tmux"];
|
|
373
|
-
const ALL_APT_DEPS = [...BASE_DEPS, ...VNC_DEPS, ...WIFI_DEPS, ...TERMINAL_DEPS];
|
|
368
|
+
const ALL_APT_DEPS = [...BASE_DEPS, ...VNC_DEPS, ...WIFI_DEPS];
|
|
374
369
|
// Task 634 — verify the "deps are present" assumption with `dpkg -s` instead
|
|
375
370
|
// of asserting it (feedback_loud_failures.md). The previous silent-skip
|
|
376
371
|
// branch was benign until Task 632 added xdotool (the first new apt dep
|
|
@@ -397,7 +392,6 @@ function installSystemDeps() {
|
|
|
397
392
|
installAptGroup("base utilities", BASE_DEPS);
|
|
398
393
|
installAptGroup("VNC stack", VNC_DEPS);
|
|
399
394
|
installAptGroup("WiFi AP", WIFI_DEPS);
|
|
400
|
-
installAptGroup("terminal", TERMINAL_DEPS);
|
|
401
395
|
}
|
|
402
396
|
// Hostname resolution — four sources, in priority order:
|
|
403
397
|
// 1. --hostname flag (unconditional — the caller is the authority)
|
|
@@ -1674,171 +1668,56 @@ function installCrons() {
|
|
|
1674
1668
|
logFile(` crontab write failed: ${write.stderr}`);
|
|
1675
1669
|
}
|
|
1676
1670
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
//
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
// own the full download / verify / install flow here.
|
|
1689
|
-
function provisionTtydBinary() {
|
|
1690
|
-
const unameRaw = spawnSync("uname", ["-m"], { encoding: "utf-8", stdio: "pipe", timeout: 5_000 });
|
|
1691
|
-
const uname = (unameRaw.stdout || "").trim();
|
|
1692
|
-
const arch = mapUnameToTtydArch(uname);
|
|
1693
|
-
if (arch === null) {
|
|
1694
|
-
console.error(` WARNING: ttyd — unsupported architecture 'uname -m'='${uname}'. Admin terminal will be unavailable.`);
|
|
1695
|
-
console.error(` Remediate: install ttyd ${TTYD_VERSION} manually for your platform and place it at ${TTYD_INSTALL_PATH}, then 'sudo chmod +x ${TTYD_INSTALL_PATH}'.`);
|
|
1696
|
-
return false;
|
|
1697
|
-
}
|
|
1698
|
-
const pinnedDigest = TTYD_SHA256_BY_ARCH[arch];
|
|
1699
|
-
const url = ttydDownloadUrl(arch);
|
|
1700
|
-
const remediation = `curl -L -o /tmp/ttyd.${arch} '${url}' && sudo mv /tmp/ttyd.${arch} ${TTYD_INSTALL_PATH} && sudo chmod +x ${TTYD_INSTALL_PATH}`;
|
|
1701
|
-
// Idempotency: existing binary with matching pinned digest → skip download.
|
|
1702
|
-
if (existsSync(TTYD_INSTALL_PATH)) {
|
|
1703
|
-
try {
|
|
1704
|
-
const existingDigest = sha256File(TTYD_INSTALL_PATH);
|
|
1705
|
-
if (existingDigest === pinnedDigest) {
|
|
1706
|
-
console.log(` ttyd ${TTYD_VERSION} already installed at ${TTYD_INSTALL_PATH} (SHA256 match — skipping download)`);
|
|
1707
|
-
return true;
|
|
1708
|
-
}
|
|
1709
|
-
console.log(` ttyd at ${TTYD_INSTALL_PATH} has different digest — replacing with pinned ${TTYD_VERSION}`);
|
|
1710
|
-
}
|
|
1711
|
-
catch (err) {
|
|
1712
|
-
console.error(` WARNING: could not read existing ${TTYD_INSTALL_PATH}: ${err instanceof Error ? err.message : String(err)} — will overwrite`);
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
if (!canSudo()) {
|
|
1716
|
-
console.error(` WARNING: ttyd — sudo unavailable non-interactively, cannot write ${TTYD_INSTALL_PATH}. Admin terminal will be unavailable.`);
|
|
1717
|
-
console.error(` Remediate: ${remediation}`);
|
|
1718
|
-
return false;
|
|
1719
|
-
}
|
|
1720
|
-
const tmpPath = `/tmp/ttyd.${arch}`;
|
|
1721
|
-
try {
|
|
1722
|
-
console.log(` Downloading ttyd ${TTYD_VERSION} for ${arch} from ${url}`);
|
|
1723
|
-
shellRetry("curl", ["-fL", "--retry", "3", "--retry-delay", "5", "-o", tmpPath, url], { timeout: 60_000 });
|
|
1724
|
-
}
|
|
1725
|
-
catch (err) {
|
|
1726
|
-
console.error(` WARNING: ttyd download failed: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
|
|
1727
|
-
console.error(` Remediate: ${remediation}`);
|
|
1728
|
-
try {
|
|
1729
|
-
unlinkSync(tmpPath);
|
|
1730
|
-
}
|
|
1731
|
-
catch { /* nothing to clean */ }
|
|
1732
|
-
return false;
|
|
1733
|
-
}
|
|
1734
|
-
let actualDigest;
|
|
1735
|
-
try {
|
|
1736
|
-
actualDigest = sha256File(tmpPath);
|
|
1737
|
-
}
|
|
1738
|
-
catch (err) {
|
|
1739
|
-
console.error(` WARNING: ttyd — could not read downloaded file ${tmpPath}: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
|
|
1740
|
-
try {
|
|
1741
|
-
unlinkSync(tmpPath);
|
|
1742
|
-
}
|
|
1743
|
-
catch { /* nothing to clean */ }
|
|
1744
|
-
return false;
|
|
1671
|
+
// Task 645 — idempotent tear-down of the Task 591 ttyd/tmux pipeline. Task 643
|
|
1672
|
+
// collapsed the admin upgrade and header-Terminal surfaces onto the existing
|
|
1673
|
+
// VNC terminal path; the parallel ttyd+xterm.js pipeline became orphan code.
|
|
1674
|
+
// Fresh devices never provision it. Devices carrying the Task 591 install
|
|
1675
|
+
// get their maxy-ttyd.service stopped, disabled, and removed on the first
|
|
1676
|
+
// post-645 installer run. Subsequent runs are silent no-ops — the absence
|
|
1677
|
+
// of the unit file short-circuits before any systemctl call is reached.
|
|
1678
|
+
function installTerminalService() {
|
|
1679
|
+
log("11", TOTAL, "Removing legacy admin terminal service (maxy-ttyd)...");
|
|
1680
|
+
if (!isLinux()) {
|
|
1681
|
+
return;
|
|
1745
1682
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
console.error(` Admin terminal will be unavailable. A later installer version may pin a newer digest.`);
|
|
1751
|
-
try {
|
|
1752
|
-
unlinkSync(tmpPath);
|
|
1753
|
-
}
|
|
1754
|
-
catch { /* nothing to clean */ }
|
|
1755
|
-
return false;
|
|
1683
|
+
const homeDir = process.env.HOME ?? "/root";
|
|
1684
|
+
const unitPath = resolve(homeDir, ".config/systemd/user/maxy-ttyd.service");
|
|
1685
|
+
if (!existsSync(unitPath)) {
|
|
1686
|
+
return;
|
|
1756
1687
|
}
|
|
1757
|
-
console.
|
|
1688
|
+
console.error(" [installer] maxy-ttyd: stopping and removing stale service (Task 645 orphan cleanup)");
|
|
1689
|
+
// Stop and disable are best-effort — systemctl returns non-zero on a unit
|
|
1690
|
+
// that's already inactive or never enabled, which is fine. stdio: "pipe"
|
|
1691
|
+
// keeps the operator terminal clean; diagnostics go to the installer log
|
|
1692
|
+
// via spawnSync's return if anyone needs them.
|
|
1693
|
+
spawnSync("systemctl", ["--user", "stop", "maxy-ttyd"], { stdio: "pipe", timeout: 10_000 });
|
|
1694
|
+
spawnSync("systemctl", ["--user", "disable", "maxy-ttyd"], { stdio: "pipe", timeout: 10_000 });
|
|
1758
1695
|
try {
|
|
1759
|
-
|
|
1760
|
-
shell("chmod", ["+x", TTYD_INSTALL_PATH], { sudo: true });
|
|
1696
|
+
unlinkSync(unitPath);
|
|
1761
1697
|
}
|
|
1762
1698
|
catch (err) {
|
|
1763
|
-
console.error(` WARNING:
|
|
1764
|
-
console.error(` Remediate: ${remediation}`);
|
|
1765
|
-
try {
|
|
1766
|
-
unlinkSync(tmpPath);
|
|
1767
|
-
}
|
|
1768
|
-
catch { /* already moved or cleaned */ }
|
|
1769
|
-
return false;
|
|
1770
|
-
}
|
|
1771
|
-
console.log(` ttyd ${TTYD_VERSION} installed at ${TTYD_INSTALL_PATH}`);
|
|
1772
|
-
return true;
|
|
1773
|
-
}
|
|
1774
|
-
function installTerminalService() {
|
|
1775
|
-
log("11", TOTAL, "Installing admin terminal service (ttyd + tmux)...");
|
|
1776
|
-
if (!isLinux()) {
|
|
1777
|
-
console.log(" Skipping admin terminal service (not Linux). On macOS start manually:");
|
|
1778
|
-
console.log(" brew install ttyd tmux && ttyd -p 7681 -i 127.0.0.1 -W tmux new-session -A -s maxy-pty");
|
|
1699
|
+
console.error(` WARNING: could not remove ${unitPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1779
1700
|
return;
|
|
1780
1701
|
}
|
|
1781
|
-
|
|
1782
|
-
//
|
|
1783
|
-
//
|
|
1784
|
-
//
|
|
1785
|
-
|
|
1786
|
-
// Default ~/.tmux.conf — only written if the operator doesn't already have
|
|
1787
|
-
// one. `history-limit 50000` is load-bearing: a closed-tab + reopen during
|
|
1788
|
-
// an upgrade must show every line the operator missed in scrollback.
|
|
1789
|
-
const homeDir = process.env.HOME ?? "/root";
|
|
1702
|
+
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "pipe", timeout: 10_000 });
|
|
1703
|
+
// ~/.tmux.conf cleanup: only remove if the contents match what the installer
|
|
1704
|
+
// seeded (exact template or exact fallback). Anything else = operator-owned,
|
|
1705
|
+
// leave untouched. Matches the Task 645 brief: "if the installer owned it,
|
|
1706
|
+
// it's been removed; if the operator owns it, untouched."
|
|
1790
1707
|
const tmuxConfDest = resolve(homeDir, ".tmux.conf");
|
|
1791
|
-
if (
|
|
1792
|
-
const tmuxConfTemplate = resolve(INSTALL_DIR, "platform/templates/dotfiles/.tmux.conf");
|
|
1708
|
+
if (existsSync(tmuxConfDest)) {
|
|
1793
1709
|
try {
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
// Fallback if the template was not in the payload for any reason —
|
|
1800
|
-
// preserves the load-bearing scrollback-size guarantee.
|
|
1801
|
-
writeFileSync(tmuxConfDest, "set -g history-limit 50000\n");
|
|
1802
|
-
console.log(` Wrote default ~/.tmux.conf (fallback — template missing)`);
|
|
1710
|
+
const contents = readFileSync(tmuxConfDest, "utf-8");
|
|
1711
|
+
const seededFallback = "set -g history-limit 50000\n";
|
|
1712
|
+
if (contents === seededFallback) {
|
|
1713
|
+
unlinkSync(tmuxConfDest);
|
|
1714
|
+
console.log(" Removed installer-seeded ~/.tmux.conf (orphan after Task 645)");
|
|
1803
1715
|
}
|
|
1804
1716
|
}
|
|
1805
1717
|
catch (err) {
|
|
1806
|
-
console.error(` WARNING:
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
// Install and enable the maxy-ttyd.service --user unit. Independent of
|
|
1810
|
-
// BRAND.serviceName — a single device runs one admin terminal regardless of
|
|
1811
|
-
// brand, because the unit binds to 127.0.0.1:7681 which only one process can
|
|
1812
|
-
// hold anyway. On a multi-brand device, the first brand's install writes the
|
|
1813
|
-
// unit and every subsequent install is a no-op (idempotent overwrite).
|
|
1814
|
-
const systemdUserDir = resolve(homeDir, ".config/systemd/user");
|
|
1815
|
-
mkdirSync(systemdUserDir, { recursive: true });
|
|
1816
|
-
// Skip systemd-unit install if the ttyd binary is not in place — enabling
|
|
1817
|
-
// a unit whose ExecStart points at a missing file just churns systemd with
|
|
1818
|
-
// restart failures.
|
|
1819
|
-
if (!ttydReady) {
|
|
1820
|
-
console.error(" Skipping maxy-ttyd.service install — ttyd binary not present. Admin terminal will be unavailable until remediated.");
|
|
1821
|
-
return;
|
|
1822
|
-
}
|
|
1823
|
-
const ttydUnitTemplate = resolve(INSTALL_DIR, "platform/templates/systemd/maxy-ttyd.service");
|
|
1824
|
-
const ttydUnitDest = join(systemdUserDir, "maxy-ttyd.service");
|
|
1825
|
-
try {
|
|
1826
|
-
if (existsSync(ttydUnitTemplate)) {
|
|
1827
|
-
writeFileSync(ttydUnitDest, readFileSync(ttydUnitTemplate, "utf-8"));
|
|
1718
|
+
console.error(` WARNING: could not inspect ~/.tmux.conf: ${err instanceof Error ? err.message : String(err)}`);
|
|
1828
1719
|
}
|
|
1829
|
-
else {
|
|
1830
|
-
console.error(` WARNING: maxy-ttyd.service template missing at ${ttydUnitTemplate} — admin terminal will not work`);
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
catch (err) {
|
|
1835
|
-
console.error(` WARNING: failed to write ${ttydUnitDest}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1836
|
-
return;
|
|
1837
1720
|
}
|
|
1838
|
-
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
|
|
1839
|
-
spawnSync("systemctl", ["--user", "enable", "maxy-ttyd"], { stdio: "inherit" });
|
|
1840
|
-
spawnSync("systemctl", ["--user", "restart", "maxy-ttyd"], { stdio: "inherit" });
|
|
1841
|
-
console.log(" maxy-ttyd.service enabled — admin terminal available on 127.0.0.1:7681");
|
|
1842
1721
|
}
|
|
1843
1722
|
function installService() {
|
|
1844
1723
|
log("12", TOTAL, `Starting ${BRAND.productName}...`);
|
|
@@ -1914,27 +1793,32 @@ function installService() {
|
|
|
1914
1793
|
// Propagate to child processes — seed-neo4j.sh reads both variables.
|
|
1915
1794
|
process.env.EMBED_DIMENSIONS = String(EMBED_DIMS);
|
|
1916
1795
|
process.env.NEO4J_URI = `bolt://localhost:${NEO4J_PORT}`;
|
|
1796
|
+
// Task 647: maxy-ui runs on an internal-only port so a restart does not
|
|
1797
|
+
// drop the public TCP socket. maxy-edge.service owns the public port and
|
|
1798
|
+
// the VNC stack; maxy-ui sits behind it on 127.0.0.1:MAXY_UI_INTERNAL_PORT.
|
|
1799
|
+
// PORT + 1 (derived) avoids a fixed-port collision if the operator chose a
|
|
1800
|
+
// non-default --port.
|
|
1801
|
+
const MAXY_UI_INTERNAL_PORT = PORT + 1;
|
|
1917
1802
|
const neo4jServiceDep = NEO4J_DEDICATED ? `neo4j-${BRAND.hostname}.service` : "neo4j.service";
|
|
1918
1803
|
const serviceFile = `[Unit]
|
|
1919
1804
|
Description=${BRAND.productName} AI Assistant
|
|
1920
|
-
After=${neo4jServiceDep}
|
|
1805
|
+
After=${neo4jServiceDep} maxy-edge.service
|
|
1806
|
+
Wants=maxy-edge.service
|
|
1921
1807
|
|
|
1922
1808
|
[Service]
|
|
1923
1809
|
Type=notify
|
|
1924
1810
|
NotifyAccess=all
|
|
1925
1811
|
WorkingDirectory=${INSTALL_DIR}/server
|
|
1926
|
-
ExecStartPre=/bin/bash ${INSTALL_DIR}/platform/scripts/vnc.sh start
|
|
1927
1812
|
ExecStartPre=-/bin/bash ${INSTALL_DIR}/platform/scripts/resume-tunnel.sh
|
|
1928
1813
|
ExecStart=/usr/bin/node --require ./server-init.cjs server.js
|
|
1929
|
-
ExecStopPost=/bin/bash ${INSTALL_DIR}/platform/scripts/vnc.sh stop
|
|
1930
1814
|
Restart=on-failure
|
|
1931
1815
|
RestartSec=5
|
|
1932
1816
|
WatchdogSec=30
|
|
1933
1817
|
TimeoutStartSec=60
|
|
1934
1818
|
TimeoutStopSec=10
|
|
1935
1819
|
Environment=NODE_ENV=production
|
|
1936
|
-
Environment=PORT=${
|
|
1937
|
-
Environment=HOSTNAME=
|
|
1820
|
+
Environment=PORT=${MAXY_UI_INTERNAL_PORT}
|
|
1821
|
+
Environment=HOSTNAME=127.0.0.1
|
|
1938
1822
|
Environment=KEEP_ALIVE_TIMEOUT=61000
|
|
1939
1823
|
Environment=DISPLAY=:99
|
|
1940
1824
|
Environment=PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
|
|
@@ -1948,6 +1832,24 @@ StandardError=append:${persistDir}/logs/server.log
|
|
|
1948
1832
|
WantedBy=default.target
|
|
1949
1833
|
`;
|
|
1950
1834
|
writeFileSync(join(serviceDir, BRAND.serviceName), serviceFile);
|
|
1835
|
+
// Task 647 — maxy-edge.service: the always-on front door that owns the
|
|
1836
|
+
// public port (PORT) and the VNC stack (Xtigervnc + websockify). Its
|
|
1837
|
+
// lifecycle is independent of maxy-ui, so an in-place upgrade triggered
|
|
1838
|
+
// from the admin terminal can restart maxy-ui without disconnecting the
|
|
1839
|
+
// browser's remote terminal WebSocket.
|
|
1840
|
+
const edgeTemplatePath = resolve(INSTALL_DIR, "platform/templates/systemd/maxy-edge.service");
|
|
1841
|
+
if (existsSync(edgeTemplatePath)) {
|
|
1842
|
+
const edgeServiceContent = readFileSync(edgeTemplatePath, "utf-8")
|
|
1843
|
+
.replace(/__INSTALL_DIR__/g, INSTALL_DIR)
|
|
1844
|
+
.replace(/__EDGE_PORT__/g, String(PORT))
|
|
1845
|
+
.replace(/__MAXY_UI_PORT__/g, String(MAXY_UI_INTERNAL_PORT))
|
|
1846
|
+
.replace(/__PERSIST_DIR__/g, persistDir);
|
|
1847
|
+
writeFileSync(join(serviceDir, "maxy-edge.service"), edgeServiceContent);
|
|
1848
|
+
logFile(` maxy-edge.service: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT}`);
|
|
1849
|
+
}
|
|
1850
|
+
else {
|
|
1851
|
+
console.error(` WARNING: maxy-edge.service template missing at ${edgeTemplatePath} — remote terminal will disconnect during upgrade`);
|
|
1852
|
+
}
|
|
1951
1853
|
// Task 560: the unit declares Environment=PATH=%h/.local/bin:... so the graph
|
|
1952
1854
|
// MCP shim's spawn("uvx", ...) resolves against uv's install location. Without
|
|
1953
1855
|
// this line, the service inherits the default systemd-user PATH — which
|
|
@@ -2000,10 +1902,19 @@ WantedBy=multi-user.target
|
|
|
2000
1902
|
spawnSync("sudo", ["loginctl", "enable-linger", currentUser], { stdio: "inherit" });
|
|
2001
1903
|
}
|
|
2002
1904
|
catch { /* not critical */ }
|
|
2003
|
-
// Reload and (re)start
|
|
1905
|
+
// Reload and (re)start.
|
|
1906
|
+
//
|
|
1907
|
+
// Task 647 ordering: on upgrades, the old maxy-ui still holds the public
|
|
1908
|
+
// port (PORT). Stop it FIRST so maxy-edge can bind that socket; starting
|
|
1909
|
+
// maxy-edge first would race against the old maxy-ui and fail with EADDRINUSE.
|
|
1910
|
+
// Fresh installs: the stop is a no-op.
|
|
2004
1911
|
const unitName = BRAND.serviceName.replace(".service", "");
|
|
1912
|
+
spawnSync("systemctl", ["--user", "stop", unitName], { stdio: "inherit" });
|
|
2005
1913
|
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
|
|
1914
|
+
spawnSync("systemctl", ["--user", "enable", "maxy-edge"], { stdio: "inherit" });
|
|
2006
1915
|
spawnSync("systemctl", ["--user", "enable", unitName], { stdio: "inherit" });
|
|
1916
|
+
// maxy-edge first: binds public port + starts VNC stack. Then maxy-ui.
|
|
1917
|
+
spawnSync("systemctl", ["--user", "restart", "maxy-edge"], { stdio: "inherit" });
|
|
2007
1918
|
spawnSync("systemctl", ["--user", "restart", unitName], { stdio: "inherit" });
|
|
2008
1919
|
// Wait for the server to come up
|
|
2009
1920
|
console.log(" Waiting for web server...");
|
|
@@ -2383,7 +2294,7 @@ try {
|
|
|
2383
2294
|
setupVncViewer();
|
|
2384
2295
|
setupAccount();
|
|
2385
2296
|
installTunnelScripts(); // ~/setup-tunnel.sh, ~/reset-tunnel.sh — the SKILL contract
|
|
2386
|
-
installTerminalService(); // Task
|
|
2297
|
+
installTerminalService(); // Task 645: tears down Task 591's orphan maxy-ttyd.service on upgrades
|
|
2387
2298
|
installService();
|
|
2388
2299
|
console.log("");
|
|
2389
2300
|
console.log("================================================================");
|
package/dist/uninstall.js
CHANGED
|
@@ -92,6 +92,15 @@ function stopServices() {
|
|
|
92
92
|
catch {
|
|
93
93
|
console.log(` ${BRAND.serviceName} not running`);
|
|
94
94
|
}
|
|
95
|
+
// Task 647: maxy-edge owns the public port and VNC stack. Stop it after
|
|
96
|
+
// maxy-ui so the edge's ExecStopPost (vnc.sh stop) runs cleanly.
|
|
97
|
+
try {
|
|
98
|
+
spawnSync("systemctl", ["--user", "stop", "maxy-edge"], { stdio: "pipe", timeout: 15_000 });
|
|
99
|
+
console.log(" Stopped maxy-edge");
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
console.log(" maxy-edge not running");
|
|
103
|
+
}
|
|
95
104
|
// Stop Neo4j
|
|
96
105
|
try {
|
|
97
106
|
spawnSync("sudo", ["systemctl", "stop", "neo4j"], { stdio: "pipe", timeout: 15_000 });
|
|
@@ -462,6 +471,21 @@ function removeSystemdService() {
|
|
|
462
471
|
console.log(` Failed to remove service file: ${err instanceof Error ? err.message : String(err)}`);
|
|
463
472
|
}
|
|
464
473
|
}
|
|
474
|
+
// Task 647: remove maxy-edge.service alongside maxy-ui.
|
|
475
|
+
try {
|
|
476
|
+
spawnSync("systemctl", ["--user", "disable", "maxy-edge"], { stdio: "pipe" });
|
|
477
|
+
}
|
|
478
|
+
catch { /* ignore */ }
|
|
479
|
+
const edgeServiceFile = resolve(HOME, ".config/systemd/user/maxy-edge.service");
|
|
480
|
+
if (existsSync(edgeServiceFile)) {
|
|
481
|
+
try {
|
|
482
|
+
rmSync(edgeServiceFile);
|
|
483
|
+
console.log(" Removed maxy-edge.service");
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
console.log(` Failed to remove maxy-edge.service: ${err instanceof Error ? err.message : String(err)}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
465
489
|
// Reload daemon
|
|
466
490
|
try {
|
|
467
491
|
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
package/package.json
CHANGED
|
@@ -68,12 +68,11 @@ The logs will show which service failed to start and why. Common causes:
|
|
|
68
68
|
|
|
69
69
|
## Systemd units on each device
|
|
70
70
|
|
|
71
|
-
Each Maxy device runs
|
|
71
|
+
Each Maxy device runs one `--user` systemd unit:
|
|
72
72
|
|
|
73
73
|
- `maxy-ui.service` — the admin + public HTTP server (default port 19200). Restarted by the upgrade flow; short downtime is expected during steps 8→12 of an upgrade.
|
|
74
|
-
- `maxy-ttyd.service` — a persistent PTY over WebSocket on `127.0.0.1:7681` attached to a tmux session named `maxy-pty`. Independent of `maxy-ui` — restarting the UI does not interrupt shell work, which is exactly the property the upgrade flow relies on to survive its own admin-server restart. The `ttyd` binary is installed at `/usr/local/bin/ttyd` from a pinned upstream GitHub release (SHA256-verified by the installer). It is NOT an apt package — Raspberry Pi OS (Bookworm-based) does not ship `ttyd` in apt.
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
The admin terminal (header Terminal button and Software Update window) runs inside the VNC framebuffer — a `gnome-terminal`/`xterm` process spawned on display `:99` or the operator's native session and rendered in the admin UI via a noVNC iframe. The terminal is ephemeral and does not have its own systemd unit. If the Software Update window fails to open or the terminal doesn't appear, check `sudo tail -n 50 ~/.maxy/logs/terminal-launch.log` — the VNC stack and spawn pipeline are the only components involved.
|
|
77
76
|
|
|
78
77
|
## Upgrading
|
|
79
78
|
|
|
@@ -311,6 +311,10 @@ Separate from the knowledge retrieval pipeline, `conversation-search` provides s
|
|
|
311
311
|
|
|
312
312
|
This tool is read-only and available to both public and admin agents.
|
|
313
313
|
|
|
314
|
+
### When conversations are created
|
|
315
|
+
|
|
316
|
+
`:Conversation` nodes on webchat (admin login, "New conversation" in the burger, a new public visitor) are created lazily. Opening the chat or logging in does not write anything to the graph — Maxy only records the conversation once the user sends a second message. This keeps `conversation-search` and the Conversations modal free of one-turn abandoned threads. WhatsApp and Telegram take the opposite posture: a first inbound DM is a committed interaction, so the graph node is created eagerly on message one. See `.docs/web-chat.md` "Deferred conversation persistence (Task 650)" for the full contract.
|
|
317
|
+
|
|
314
318
|
---
|
|
315
319
|
|
|
316
320
|
## Context Assembly — How Retrieved Knowledge Reaches the Agent
|
|
@@ -52,7 +52,7 @@ The memory graph is stored on your Pi. It never leaves your network.
|
|
|
52
52
|
|
|
53
53
|
## The Web Interface
|
|
54
54
|
|
|
55
|
-
The web app runs on your Pi on port 19200. It provides:
|
|
55
|
+
The web app runs on your Pi on port 19200. A small always-on front door (`maxy-edge`) owns that port and the remote terminal transport — so when the Software Update command restarts the app server, the browser-side terminal keeps streaming bytes exactly like an SSH session would. It provides:
|
|
56
56
|
|
|
57
57
|
- **Admin chat** (at `/`) — your primary interface, PIN-protected
|
|
58
58
|
- **Public chat** (at `/{agent-name}`) — visitor-facing agents, each with their own URL. On public hostnames, the root path serves the default agent.
|
|
@@ -124,7 +124,7 @@ DISPLAY=:99 xdpyinfo >/dev/null 2>&1 && echo "display :99 ok" || echo "display d
|
|
|
124
124
|
**Common upgrade-specific failures:**
|
|
125
125
|
|
|
126
126
|
- `err="no terminal emulator installed"` → run `sudo apt-get install -y xterm xdotool` or re-run `npx -y @rubytech/create-maxy@latest` — the installer provisions both as hard deps.
|
|
127
|
-
- `err="spawn detached but no terminal PID visible within 1s" ... reason=upgrade` → X server on `:99` is wedged
|
|
127
|
+
- `err="spawn detached but no terminal PID visible within 1s" ... reason=upgrade` → X server on `:99` is wedged. Since Task 647 the VNC stack is owned by `maxy-edge`, so the recovery is `sudo systemctl --user restart maxy-edge` (cycles `vnc.sh start` via that unit's ExecStartPre). Then click **Try again** in the modal.
|
|
128
128
|
- `err="window absent from target display after spawn" ... reason=upgrade` → the spawn succeeded but landed on the wrong display (Task 632 class). Re-run the installer to refresh the `resolve_terminal_bin` logic. If a stale `gnome-terminal` is hitting `:99` via D-Bus delegation, the installer's `xterm` fallback is the fix.
|
|
129
129
|
- `err="xdotool not installed — re-run installer to repair"` → Task 634 preflight. `sudo apt-get install -y xdotool` or re-run the installer.
|
|
130
130
|
- `err="VNC failed to start after recovery attempt"` → `Xtigervnc` itself is not coming up. Inspect `~/.maxy/logs/vnc-boot.log` for tigervnc startup lines; `ss -ltn '( sport = 6080 or sport = 5900 )'` should show both ports listening.
|
|
@@ -158,7 +158,7 @@ If the `cmd=` field does not contain `-e bash -c`, re-run the installer — the
|
|
|
158
158
|
|
|
159
159
|
**Symptom:** You opened the burger menu and clicked **Terminal**, but instead of the overlay appearing you got an inline error like "Terminal failed to start" or "VNC failed to start".
|
|
160
160
|
|
|
161
|
-
**What it means:** `POST /api/admin/terminal/launch` returned a 502 — either the VNC stack on port 5900 is down or the terminal emulator could not be spawned on display `:99`.
|
|
161
|
+
**What it means:** `POST /api/admin/terminal/launch` returned a 502 — either the VNC stack on port 5900 is down or the terminal emulator could not be spawned on display `:99`. This is a VNC-stack or display-spawn failure.
|
|
162
162
|
|
|
163
163
|
Step-by-step diagnosis:
|
|
164
164
|
|
|
@@ -212,9 +212,8 @@ start_chrome() {
|
|
|
212
212
|
# on the right display here — no bug reachable on loopback)
|
|
213
213
|
#
|
|
214
214
|
# Prints "<bin>\t<flags>" on stdout (tab-separated), or exits non-zero with a
|
|
215
|
-
# loud-fail log if neither is installed
|
|
216
|
-
#
|
|
217
|
-
# retains `--wait` (see Task 627 pgrep-visibility fix). xterm takes no flag.
|
|
215
|
+
# loud-fail log if neither is installed. gnome-terminal retains `--wait`
|
|
216
|
+
# (see Task 627 pgrep-visibility fix). xterm takes no flag.
|
|
218
217
|
# xterm flags for the :99 VNC framebuffer. Fontconfig alias `monospace`
|
|
219
218
|
# resolves via /etc/fonts to the distro's default monospace face (DejaVu
|
|
220
219
|
# Sans Mono on Ubuntu/Debian) — no quoting needed, so the flag string
|
|
@@ -494,17 +493,39 @@ start_terminal_upgrade_on() {
|
|
|
494
493
|
;;
|
|
495
494
|
esac
|
|
496
495
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
#
|
|
500
|
-
#
|
|
501
|
-
#
|
|
502
|
-
#
|
|
503
|
-
#
|
|
504
|
-
|
|
505
|
-
|
|
496
|
+
# Task 647: the upgrade command (`npx -y @rubytech/create-maxy@latest`) calls
|
|
497
|
+
# `systemctl --user restart maxy-ui` partway through. If the terminal shell
|
|
498
|
+
# lives in maxy-ui's cgroup it gets SIGKILL'd by that restart and the install
|
|
499
|
+
# aborts. `systemd-run --user --scope` places the shell in its own transient
|
|
500
|
+
# scope unit whose lifetime is independent of whichever service triggered
|
|
501
|
+
# vnc.sh. `setsid -f` is no longer needed — scope units handle detachment.
|
|
502
|
+
# Fallback (no systemd-run): `setsid -f`, with the known caveat that the
|
|
503
|
+
# caller must not be inside a unit scheduled for restart during the install.
|
|
504
|
+
local run_prefix=""
|
|
505
|
+
if command -v systemd-run >/dev/null 2>&1; then
|
|
506
|
+
run_prefix="systemd-run --user --quiet --scope --unit=maxy-upgrade-terminal-$$.scope --collect --"
|
|
507
|
+
log "Wrapping upgrade terminal in systemd-run --user --scope (Task 647)"
|
|
508
|
+
else
|
|
509
|
+
log "WARNING: systemd-run not available — falling back to setsid -f (terminal may die on maxy-ui restart)"
|
|
510
|
+
fi
|
|
511
|
+
log "Starting ${run_prefix} ${bin} ${flags} ${dispatch_flag} bash -c \"${upgrade_cmd}; exec bash\" on ${target_display} (${label}) reason=upgrade"
|
|
512
|
+
|
|
513
|
+
# $flags stays unquoted for word-splitting (xterm's flags have multiple
|
|
514
|
+
# tokens); bash_wrapper stays quoted so the whole command string reaches
|
|
515
|
+
# bash -c as one argument. $run_prefix is unquoted so its words expand.
|
|
516
|
+
if [ -n "$run_prefix" ]; then
|
|
517
|
+
if [ -n "$flags" ]; then
|
|
518
|
+
DISPLAY="${target_display}" $run_prefix "$bin" $flags "$dispatch_flag" bash -c "$bash_wrapper" >> "$TERMINAL_LOG" 2>&1 &
|
|
519
|
+
else
|
|
520
|
+
DISPLAY="${target_display}" $run_prefix "$bin" "$dispatch_flag" bash -c "$bash_wrapper" >> "$TERMINAL_LOG" 2>&1 &
|
|
521
|
+
fi
|
|
522
|
+
disown 2>/dev/null || true
|
|
506
523
|
else
|
|
507
|
-
|
|
524
|
+
if [ -n "$flags" ]; then
|
|
525
|
+
DISPLAY="${target_display}" setsid -f "$bin" $flags "$dispatch_flag" bash -c "$bash_wrapper" >> "$TERMINAL_LOG" 2>&1 || true
|
|
526
|
+
else
|
|
527
|
+
DISPLAY="${target_display}" setsid -f "$bin" "$dispatch_flag" bash -c "$bash_wrapper" >> "$TERMINAL_LOG" 2>&1 || true
|
|
528
|
+
fi
|
|
508
529
|
fi
|
|
509
530
|
|
|
510
531
|
if ! wait_for_terminal; then
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Maxy Edge (public port + VNC transport — independent of maxy-ui)
|
|
3
|
+
# No ordering dependency on maxy-ui: the edge is the long-running front door.
|
|
4
|
+
# maxy-ui.service declares Wants/After this unit so maxy-ui only starts once
|
|
5
|
+
# the edge is listening, but this unit has no such reciprocal dependency.
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=simple
|
|
9
|
+
# ExecStartPre owns the VNC stack so an `npx -y @rubytech/create-maxy@latest`
|
|
10
|
+
# run from the admin terminal can restart maxy-ui without taking Xtigervnc,
|
|
11
|
+
# websockify, or the upgrade shell down with it. Task 647.
|
|
12
|
+
ExecStartPre=/bin/bash __INSTALL_DIR__/platform/scripts/vnc.sh start
|
|
13
|
+
ExecStart=/usr/bin/node __INSTALL_DIR__/server/maxy-edge.js
|
|
14
|
+
ExecStopPost=/bin/bash __INSTALL_DIR__/platform/scripts/vnc.sh stop
|
|
15
|
+
Restart=on-failure
|
|
16
|
+
RestartSec=5
|
|
17
|
+
TimeoutStartSec=60
|
|
18
|
+
TimeoutStopSec=10
|
|
19
|
+
Environment=NODE_ENV=production
|
|
20
|
+
Environment=EDGE_PORT=__EDGE_PORT__
|
|
21
|
+
Environment=EDGE_HOSTNAME=0.0.0.0
|
|
22
|
+
Environment=MAXY_UI_HOST=127.0.0.1
|
|
23
|
+
Environment=MAXY_UI_PORT=__MAXY_UI_PORT__
|
|
24
|
+
Environment=WEBSOCKIFY_HOST=127.0.0.1
|
|
25
|
+
Environment=WEBSOCKIFY_PORT=6080
|
|
26
|
+
Environment=DISPLAY=:99
|
|
27
|
+
Environment=MAXY_PLATFORM_ROOT=__INSTALL_DIR__/platform
|
|
28
|
+
Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
29
|
+
StandardOutput=append:__PERSIST_DIR__/logs/edge.log
|
|
30
|
+
StandardError=append:__PERSIST_DIR__/logs/edge.log
|
|
31
|
+
|
|
32
|
+
[Install]
|
|
33
|
+
WantedBy=default.target
|
|
@@ -401,6 +401,8 @@ function forwardUpgrade(req, clientSocket, head) {
|
|
|
401
401
|
var server = createServer((req, res) => {
|
|
402
402
|
forwardHttp(req, res);
|
|
403
403
|
});
|
|
404
|
+
server.keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT ?? "61000", 10);
|
|
405
|
+
server.headersTimeout = server.keepAliveTimeout + 1e3;
|
|
404
406
|
attachVncWsProxy(server, {
|
|
405
407
|
isPublicHost,
|
|
406
408
|
upstreamHost: WEBSOCKIFY_HOST,
|