@rubytech/create-realagent 1.0.684 → 1.0.686

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 (36) hide show
  1. package/dist/index.js +49 -203
  2. package/dist/pinned-binaries.js +10 -41
  3. package/dist/uninstall.js +47 -19
  4. package/package.json +1 -1
  5. package/payload/platform/config/brand.json +1 -0
  6. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +35 -9
  7. package/payload/platform/plugins/docs/PLUGIN.md +2 -0
  8. package/payload/platform/plugins/docs/references/cloudflare.md +1 -1
  9. package/payload/platform/plugins/docs/references/deployment.md +19 -6
  10. package/payload/platform/plugins/docs/references/graph.md +38 -0
  11. package/payload/platform/plugins/docs/references/platform.md +10 -7
  12. package/payload/platform/plugins/docs/references/troubleshooting.md +23 -13
  13. package/payload/platform/scripts/vnc.sh +7 -7
  14. package/payload/platform/templates/systemd/{maxy-edge.service → edge.service.template} +9 -6
  15. package/payload/server/maxy-edge.js +7 -369
  16. package/payload/server/public/assets/admin-BqLtaMVu.js +352 -0
  17. package/payload/server/public/assets/{data-DUSyrydY.js → data-BZ7v-zug.js} +1 -1
  18. package/payload/server/public/assets/{file-CDJ6dUV3.js → file-CScYkZq5.js} +1 -1
  19. package/payload/server/public/assets/graph-tjXdtwk-.js +50 -0
  20. package/payload/server/public/assets/{house-CNP_bwvT.js → house-CdFRNujU.js} +1 -1
  21. package/payload/server/public/assets/{jsx-runtime-BFFQvkdQ.css → jsx-runtime-Og0q7dXg.css} +1 -1
  22. package/payload/server/public/assets/{public-sHoAccvb.js → public-CrkQJek6.js} +2 -2
  23. package/payload/server/public/assets/{share-2-DBcb9j6E.js → share-2-Ev-D4Lm9.js} +1 -1
  24. package/payload/server/public/assets/{useVoiceRecorder-CtSgpc95.js → useVoiceRecorder-DyDXH7EA.js} +2 -2
  25. package/payload/server/public/assets/{x-CTVJaC_u.js → x-D5W7ddgP.js} +1 -1
  26. package/payload/server/public/data.html +6 -6
  27. package/payload/server/public/graph.html +6 -6
  28. package/payload/server/public/index.html +7 -8
  29. package/payload/server/public/public.html +4 -4
  30. package/payload/server/server.js +830 -258
  31. package/payload/platform/templates/dotfiles/.tmux.conf +0 -1
  32. package/payload/platform/templates/systemd/maxy-ttyd.service +0 -25
  33. package/payload/server/public/assets/admin-WQxJgaus.js +0 -362
  34. package/payload/server/public/assets/admin-kHJ-D0s7.css +0 -1
  35. package/payload/server/public/assets/graph-CWcYp5bE.js +0 -50
  36. /package/payload/server/public/assets/{jsx-runtime-BVKWELH6.js → jsx-runtime-CHqDsKlc.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, createHash } from "node:crypto";
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.
@@ -347,7 +346,7 @@ function installAptGroup(label, pkgs) {
347
346
  // ---------------------------------------------------------------------------
348
347
  // Installation steps
349
348
  // ---------------------------------------------------------------------------
350
- const TOTAL = "12";
349
+ const TOTAL = "11";
351
350
  function installSystemDeps() {
352
351
  log("1", TOTAL, "System dependencies and network...");
353
352
  if (!isLinux()) {
@@ -366,12 +365,12 @@ function installSystemDeps() {
366
365
  // assertion in vnc.sh check_window_on_display, closing the silent-fail
367
366
  // class where PID is alive but no window is mapped on the target display.
368
367
  const VNC_DEPS = ["tigervnc-standalone-server", "python3-websockify", "novnc", "xdg-utils", "chromium", "xterm", "xdotool"];
369
- // Task 657: tmux powers the byte-stream admin terminal. ttyd attaches the
370
- // shared `maxy-pty` tmux session, so scrollback survives WS reconnects and
371
- // the same session is reused by the header overlay + upgrade modal.
372
- const TERMINAL_DEPS = ["tmux"];
368
+ // Task 664 retired the ttyd/tmux admin terminal stack upgrades run via
369
+ // the action runner (systemd-run --user transient units) and no longer
370
+ // need a shared tmux session. `tmux` was only required by the retired
371
+ // byte-stream terminal; removing it shrinks the apt footprint.
373
372
  const WIFI_DEPS = ["hostapd", "dnsmasq"];
374
- const ALL_APT_DEPS = [...BASE_DEPS, ...VNC_DEPS, ...TERMINAL_DEPS, ...WIFI_DEPS];
373
+ const ALL_APT_DEPS = [...BASE_DEPS, ...VNC_DEPS, ...WIFI_DEPS];
375
374
  // Task 634 — verify the "deps are present" assumption with `dpkg -s` instead
376
375
  // of asserting it (feedback_loud_failures.md). The previous silent-skip
377
376
  // branch was benign until Task 632 added xdotool (the first new apt dep
@@ -1590,183 +1589,16 @@ function installCrons() {
1590
1589
  logFile(` crontab write failed: ${write.stderr}`);
1591
1590
  }
1592
1591
  }
1593
- // Task 657 restored the Task-591 ttyd/tmux pipeline after Task 645's
1594
- // tear-down. Rationale: Task 643 collapsed the upgrade surface onto VNC, but
1595
- // the RFB + X-focus path silently drops keystrokes at `[sudo] password for`.
1596
- // The byte-stream surface (ttyd + tmux + xterm.js) is SSH-equivalent — the
1597
- // operator's stated success case and is now attached to `maxy-edge.service`
1598
- // so the WS transport survives `systemctl --user restart maxy-ui` during an
1599
- // in-browser upgrade (Task 647 invariant holds by construction).
1600
- const TTYD_INSTALL_PATH = "/usr/local/bin/ttyd";
1601
- function sha256File(path) {
1602
- const hash = createHash("sha256");
1603
- hash.update(readFileSync(path));
1604
- return hash.digest("hex");
1605
- }
1606
- // Provision the upstream ttyd binary into /usr/local/bin/ttyd. Degrades with
1607
- // a loud warning and a copy-pasteable remediation command on any failure —
1608
- // never throws. Contract: the caller (installTerminalService) uses the
1609
- // presence of TTYD_INSTALL_PATH after return to decide whether to enable the
1610
- // maxy-ttyd.service systemd unit. ttyd is NOT in Debian Bookworm apt, so we
1611
- // own the full download / verify / install flow here.
1612
- function provisionTtydBinary() {
1613
- const unameRaw = spawnSync("uname", ["-m"], { encoding: "utf-8", stdio: "pipe", timeout: 5_000 });
1614
- const uname = (unameRaw.stdout || "").trim();
1615
- const arch = mapUnameToTtydArch(uname);
1616
- if (arch === null) {
1617
- console.error(` WARNING: ttyd — unsupported architecture 'uname -m'='${uname}'. Admin terminal will be unavailable.`);
1618
- 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}'.`);
1619
- return false;
1620
- }
1621
- const pinnedDigest = TTYD_SHA256_BY_ARCH[arch];
1622
- const url = ttydDownloadUrl(arch);
1623
- const remediation = `curl -L -o /tmp/ttyd.${arch} '${url}' && sudo mv /tmp/ttyd.${arch} ${TTYD_INSTALL_PATH} && sudo chmod +x ${TTYD_INSTALL_PATH}`;
1624
- // Idempotency: existing binary with matching pinned digest → skip download.
1625
- if (existsSync(TTYD_INSTALL_PATH)) {
1626
- try {
1627
- const existingDigest = sha256File(TTYD_INSTALL_PATH);
1628
- if (existingDigest === pinnedDigest) {
1629
- console.log(` ttyd ${TTYD_VERSION} already installed at ${TTYD_INSTALL_PATH} (SHA256 match — skipping download)`);
1630
- return true;
1631
- }
1632
- console.log(` ttyd at ${TTYD_INSTALL_PATH} has different digest — replacing with pinned ${TTYD_VERSION}`);
1633
- }
1634
- catch (err) {
1635
- console.error(` WARNING: could not read existing ${TTYD_INSTALL_PATH}: ${err instanceof Error ? err.message : String(err)} — will overwrite`);
1636
- }
1637
- }
1638
- if (!canSudo()) {
1639
- console.error(` WARNING: ttyd — sudo unavailable non-interactively, cannot write ${TTYD_INSTALL_PATH}. Admin terminal will be unavailable.`);
1640
- console.error(` Remediate: ${remediation}`);
1641
- return false;
1642
- }
1643
- const tmpPath = `/tmp/ttyd.${arch}`;
1644
- try {
1645
- console.log(` Downloading ttyd ${TTYD_VERSION} for ${arch} from ${url}`);
1646
- shellRetry("curl", ["-fL", "--retry", "3", "--retry-delay", "5", "-o", tmpPath, url], { timeout: 60_000 });
1647
- }
1648
- catch (err) {
1649
- console.error(` WARNING: ttyd download failed: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
1650
- console.error(` Remediate: ${remediation}`);
1651
- try {
1652
- unlinkSync(tmpPath);
1653
- }
1654
- catch { /* nothing to clean */ }
1655
- return false;
1656
- }
1657
- let actualDigest;
1658
- try {
1659
- actualDigest = sha256File(tmpPath);
1660
- }
1661
- catch (err) {
1662
- console.error(` WARNING: ttyd — could not read downloaded file ${tmpPath}: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
1663
- try {
1664
- unlinkSync(tmpPath);
1665
- }
1666
- catch { /* nothing to clean */ }
1667
- return false;
1668
- }
1669
- if (actualDigest !== pinnedDigest) {
1670
- console.error(` WARNING: ttyd SHA256 mismatch — refusing to install unverified binary.`);
1671
- console.error(` expected: ${pinnedDigest}`);
1672
- console.error(` actual: ${actualDigest}`);
1673
- console.error(` Admin terminal will be unavailable. A later installer version may pin a newer digest.`);
1674
- try {
1675
- unlinkSync(tmpPath);
1676
- }
1677
- catch { /* nothing to clean */ }
1678
- return false;
1679
- }
1680
- console.log(` ttyd ${TTYD_VERSION} SHA256 verified (${actualDigest.slice(0, 12)}…)`);
1681
- try {
1682
- console.log(` [privileged] install ttyd binary to ${TTYD_INSTALL_PATH}`);
1683
- shell("mv", [tmpPath, TTYD_INSTALL_PATH], { sudo: true });
1684
- console.log(` [privileged] chmod +x ${TTYD_INSTALL_PATH}`);
1685
- shell("chmod", ["+x", TTYD_INSTALL_PATH], { sudo: true });
1686
- }
1687
- catch (err) {
1688
- console.error(` WARNING: ttyd — could not install to ${TTYD_INSTALL_PATH}: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
1689
- console.error(` Remediate: ${remediation}`);
1690
- try {
1691
- unlinkSync(tmpPath);
1692
- }
1693
- catch { /* already moved or cleaned */ }
1694
- return false;
1695
- }
1696
- console.log(` ttyd ${TTYD_VERSION} installed at ${TTYD_INSTALL_PATH}`);
1697
- return true;
1698
- }
1699
- function installTerminalService() {
1700
- log("11", TOTAL, "Installing admin terminal service (ttyd + tmux)...");
1701
- if (!isLinux()) {
1702
- console.log(" Skipping admin terminal service (not Linux). On macOS start manually:");
1703
- console.log(" brew install ttyd tmux && ttyd -p 7681 -i 127.0.0.1 -W tmux new-session -A -s maxy-pty");
1704
- return;
1705
- }
1706
- // ttyd is provisioned from upstream GitHub releases (pinned + SHA256-verified)
1707
- // because Debian Bookworm's apt does NOT carry a ttyd package (Task 602).
1708
- // A failure here is loud but non-fatal — the rest of the install completes
1709
- // and the admin UI degrades to "terminal unavailable" per Task 603.
1710
- const ttydReady = provisionTtydBinary();
1711
- // Default ~/.tmux.conf — only written if the operator doesn't already have
1712
- // one. `history-limit 50000` is load-bearing: a closed-tab + reopen during
1713
- // an upgrade must show every line the operator missed in scrollback.
1714
- const homeDir = process.env.HOME ?? "/root";
1715
- const tmuxConfDest = resolve(homeDir, ".tmux.conf");
1716
- if (!existsSync(tmuxConfDest)) {
1717
- const tmuxConfTemplate = resolve(INSTALL_DIR, "platform/templates/dotfiles/.tmux.conf");
1718
- try {
1719
- if (existsSync(tmuxConfTemplate)) {
1720
- writeFileSync(tmuxConfDest, readFileSync(tmuxConfTemplate, "utf-8"));
1721
- console.log(` Wrote default ~/.tmux.conf (history-limit 50000)`);
1722
- }
1723
- else {
1724
- // Fallback if the template was not in the payload for any reason —
1725
- // preserves the load-bearing scrollback-size guarantee.
1726
- writeFileSync(tmuxConfDest, "set -g history-limit 50000\n");
1727
- console.log(` Wrote default ~/.tmux.conf (fallback — template missing)`);
1728
- }
1729
- }
1730
- catch (err) {
1731
- console.error(` WARNING: failed to write ~/.tmux.conf: ${err instanceof Error ? err.message : String(err)}`);
1732
- }
1733
- }
1734
- // Install and enable the maxy-ttyd.service --user unit. Independent of
1735
- // BRAND.serviceName — a single device runs one admin terminal regardless of
1736
- // brand, because the unit binds to 127.0.0.1:7681 which only one process can
1737
- // hold anyway. On a multi-brand device, the first brand's install writes the
1738
- // unit and every subsequent install is a no-op (idempotent overwrite).
1739
- const systemdUserDir = resolve(homeDir, ".config/systemd/user");
1740
- mkdirSync(systemdUserDir, { recursive: true });
1741
- // Skip systemd-unit install if the ttyd binary is not in place — enabling
1742
- // a unit whose ExecStart points at a missing file just churns systemd with
1743
- // restart failures.
1744
- if (!ttydReady) {
1745
- console.error(" Skipping maxy-ttyd.service install — ttyd binary not present. Admin terminal will be unavailable until remediated.");
1746
- return;
1747
- }
1748
- const ttydUnitTemplate = resolve(INSTALL_DIR, "platform/templates/systemd/maxy-ttyd.service");
1749
- const ttydUnitDest = join(systemdUserDir, "maxy-ttyd.service");
1750
- try {
1751
- if (existsSync(ttydUnitTemplate)) {
1752
- writeFileSync(ttydUnitDest, readFileSync(ttydUnitTemplate, "utf-8"));
1753
- }
1754
- else {
1755
- console.error(` WARNING: maxy-ttyd.service template missing at ${ttydUnitTemplate} — admin terminal will not work`);
1756
- return;
1757
- }
1758
- }
1759
- catch (err) {
1760
- console.error(` WARNING: failed to write ${ttydUnitDest}: ${err instanceof Error ? err.message : String(err)}`);
1761
- return;
1762
- }
1763
- spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
1764
- spawnSync("systemctl", ["--user", "enable", "maxy-ttyd"], { stdio: "inherit" });
1765
- spawnSync("systemctl", ["--user", "restart", "maxy-ttyd"], { stdio: "inherit" });
1766
- console.log(" maxy-ttyd.service enabled — admin terminal available on 127.0.0.1:7681");
1767
- }
1592
+ // Task 664 retired the ttyd/tmux/xterm admin terminal stack. Upgrades run
1593
+ // via the action runner `systemd-run --user` transient units spawned by
1594
+ // POST /api/admin/actions/upgrade whose lifetime is independent of
1595
+ // maxy-ui, achieving the Task 647 invariant structurally rather than via
1596
+ // a peer edge service proxying a ttyd process. The installer no longer
1597
+ // provisions the ttyd binary, writes a tmux conf, or installs a ttyd
1598
+ // systemd unit. The corresponding admin UI (RemoteTerminal, TerminalOverlay,
1599
+ // xterm.js) was deleted in the same task.
1768
1600
  function installService() {
1769
- log("12", TOTAL, `Starting ${BRAND.productName}...`);
1601
+ log("11", TOTAL, `Starting ${BRAND.productName}...`);
1770
1602
  if (!isLinux()) {
1771
1603
  console.log(" Skipping systemd service (not Linux). Start manually with:");
1772
1604
  console.log(` cd ${INSTALL_DIR}/server && MAXY_PLATFORM_ROOT=${INSTALL_DIR}/platform PORT=${PORT} HOSTNAME=0.0.0.0 KEEP_ALIVE_TIMEOUT=61000 node --require ./server-init.cjs server.js`);
@@ -1847,10 +1679,12 @@ function installService() {
1847
1679
  // non-default --port.
1848
1680
  const MAXY_UI_INTERNAL_PORT = PORT + 1;
1849
1681
  const neo4jServiceDep = NEO4J_DEDICATED ? `neo4j-${BRAND.hostname}.service` : "neo4j.service";
1682
+ const edgeUnitShort = `${BRAND.hostname}-edge`;
1683
+ const edgeUnitName = `${edgeUnitShort}.service`;
1850
1684
  const serviceFile = `[Unit]
1851
1685
  Description=${BRAND.productName} AI Assistant
1852
- After=${neo4jServiceDep} maxy-edge.service
1853
- Wants=maxy-edge.service
1686
+ After=${neo4jServiceDep} ${edgeUnitName}
1687
+ Wants=${edgeUnitName}
1854
1688
 
1855
1689
  [Service]
1856
1690
  Type=notify
@@ -1879,23 +1713,30 @@ StandardError=append:${persistDir}/logs/server.log
1879
1713
  WantedBy=default.target
1880
1714
  `;
1881
1715
  writeFileSync(join(serviceDir, BRAND.serviceName), serviceFile);
1882
- // Task 647 — maxy-edge.service: the always-on front door that owns the
1883
- // public port (PORT) and the VNC stack (Xtigervnc + websockify). Its
1884
- // lifecycle is independent of maxy-ui, so an in-place upgrade triggered
1885
- // from the admin terminal can restart maxy-ui without disconnecting the
1886
- // browser's remote terminal WebSocket.
1887
- const edgeTemplatePath = resolve(INSTALL_DIR, "platform/templates/systemd/maxy-edge.service");
1716
+ // Task 647 — the edge service: always-on front door that owns the public
1717
+ // port (PORT) and the VNC stack (Xtigervnc + websockify). Its lifecycle is
1718
+ // independent of the main brand service, so an in-place upgrade triggered
1719
+ // from the admin terminal can restart the main brand service without
1720
+ // disconnecting the browser's remote terminal WebSocket.
1721
+ //
1722
+ // Task 662: the unit is per-brand so two brands on the same device each
1723
+ // own their own edge listener on their own EDGE_PORT — installing brand B
1724
+ // never rewrites brand A's unit or steals brand A's public port. Upgrades
1725
+ // from pre-662 installs require the manual recovery paragraph in
1726
+ // .docs/deployment.md before re-running this installer; auto-migration is
1727
+ // intentionally scoped out.
1728
+ const edgeTemplatePath = resolve(INSTALL_DIR, "platform/templates/systemd/edge.service.template");
1888
1729
  if (existsSync(edgeTemplatePath)) {
1889
1730
  const edgeServiceContent = readFileSync(edgeTemplatePath, "utf-8")
1890
1731
  .replace(/__INSTALL_DIR__/g, INSTALL_DIR)
1891
1732
  .replace(/__EDGE_PORT__/g, String(PORT))
1892
1733
  .replace(/__MAXY_UI_PORT__/g, String(MAXY_UI_INTERNAL_PORT))
1893
1734
  .replace(/__PERSIST_DIR__/g, persistDir);
1894
- writeFileSync(join(serviceDir, "maxy-edge.service"), edgeServiceContent);
1895
- logFile(` maxy-edge.service: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT}`);
1735
+ writeFileSync(join(serviceDir, edgeUnitName), edgeServiceContent);
1736
+ logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT}`);
1896
1737
  }
1897
1738
  else {
1898
- console.error(` WARNING: maxy-edge.service template missing at ${edgeTemplatePath} — remote terminal will disconnect during upgrade`);
1739
+ console.error(` WARNING: edge.service.template missing at ${edgeTemplatePath} — VNC transport unavailable`);
1899
1740
  }
1900
1741
  // Task 560: the unit declares Environment=PATH=%h/.local/bin:... so the graph
1901
1742
  // MCP shim's spawn("uvx", ...) resolves against uv's install location. Without
@@ -1952,17 +1793,17 @@ WantedBy=multi-user.target
1952
1793
  catch { /* not critical */ }
1953
1794
  // Reload and (re)start.
1954
1795
  //
1955
- // Task 647 ordering: on upgrades, the old maxy-ui still holds the public
1956
- // port (PORT). Stop it FIRST so maxy-edge can bind that socket; starting
1957
- // maxy-edge first would race against the old maxy-ui and fail with EADDRINUSE.
1958
- // Fresh installs: the stop is a no-op.
1796
+ // Task 647 ordering: on upgrades, the old main brand service still holds
1797
+ // the public port (PORT). Stop it FIRST so the edge can bind that socket;
1798
+ // starting the edge first would race against the old main brand service
1799
+ // and fail with EADDRINUSE. Fresh installs: the stop is a no-op.
1959
1800
  const unitName = BRAND.serviceName.replace(".service", "");
1960
1801
  spawnSync("systemctl", ["--user", "stop", unitName], { stdio: "inherit" });
1961
1802
  spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
1962
- spawnSync("systemctl", ["--user", "enable", "maxy-edge"], { stdio: "inherit" });
1803
+ spawnSync("systemctl", ["--user", "enable", edgeUnitShort], { stdio: "inherit" });
1963
1804
  spawnSync("systemctl", ["--user", "enable", unitName], { stdio: "inherit" });
1964
- // maxy-edge first: binds public port + starts VNC stack. Then maxy-ui.
1965
- spawnSync("systemctl", ["--user", "restart", "maxy-edge"], { stdio: "inherit" });
1805
+ // edge first: binds public port + starts VNC stack. Then main brand service.
1806
+ spawnSync("systemctl", ["--user", "restart", edgeUnitShort], { stdio: "inherit" });
1966
1807
  spawnSync("systemctl", ["--user", "restart", unitName], { stdio: "inherit" });
1967
1808
  // Wait for the server to come up
1968
1809
  console.log(" Waiting for web server...");
@@ -2312,6 +2153,12 @@ else {
2312
2153
  }
2313
2154
  // Dedicated = port differs from the default shared instance
2314
2155
  const NEO4J_DEDICATED = NEO4J_PORT !== DEFAULT_NEO4J_PORT;
2156
+ // ---------------------------------------------------------------------------
2157
+ // Task 664 removed the per-brand ttyd port — the admin terminal stack
2158
+ // (ttyd, tmux, xterm.js) was retired in favour of the action runner that
2159
+ // spawns transient `systemd-run --user` units per upgrade or setup-tunnel
2160
+ // invocation. No TCP listener on the device needs to be reserved for an
2161
+ // interactive-shell surface any more.
2315
2162
  const PKG_VERSION = JSON.parse(readFileSync(resolve(import.meta.dirname, "../package.json"), "utf-8")).version;
2316
2163
  initLogging();
2317
2164
  console.log("================================================================");
@@ -2344,7 +2191,6 @@ try {
2344
2191
  setupVncViewer();
2345
2192
  setupAccount();
2346
2193
  installTunnelScripts(); // ~/setup-tunnel.sh, ~/reset-tunnel.sh — the SKILL contract
2347
- installTerminalService(); // Task 657: installs maxy-ttyd.service (ttyd + tmux) for byte-stream admin terminal
2348
2194
  installService();
2349
2195
  console.log("");
2350
2196
  console.log("================================================================");
@@ -1,43 +1,12 @@
1
1
  // Pinned upstream binaries installed by the Linux provisioner.
2
2
  //
3
- // Some binaries we depend on are not available in Debian Bookworm's apt repo
4
- // (e.g. ttyd entered Debian trixie and Ubuntu 22.04+ but NOT Bookworm, which
5
- // is the base of current Raspberry Pi OS). For those, we download the upstream
6
- // prebuilt static binary from GitHub releases, SHA256-verified, and place it
7
- // under /usr/local/bin. This module is the single source of truth for every
8
- // such dependency version bumps are a one-file change.
9
- //
10
- // Verification rule: on SHA256 mismatch the installer aborts the download
11
- // step with a loud error and never writes the binary. No silent-install of
12
- // unverified bytes. See packages/create-maxy/src/index.ts → provisionTtydBinary.
13
- export const TTYD_VERSION = '1.7.7';
14
- // Asset file name as published on github.com/tsl0922/ttyd/releases.
15
- export const TTYD_ASSET_BY_ARCH = {
16
- aarch64: 'ttyd.aarch64',
17
- arm: 'ttyd.arm',
18
- x86_64: 'ttyd.x86_64',
19
- };
20
- // SHA256 digest of each asset for TTYD_VERSION. Computed by downloading the
21
- // release asset directly from GitHub and running `sha256sum`. Bump together
22
- // with TTYD_VERSION — never update one without the other.
23
- export const TTYD_SHA256_BY_ARCH = {
24
- aarch64: 'b38acadd89d1d396a0f5649aa52c539edbad07f4bc7348b27b4f4b7219dd4165',
25
- arm: '05eac1223914f18c65898d72c8d14e76bbb5435f7762c6dc7f16f041994a8109',
26
- x86_64: '8a217c968aba172e0dbf3f34447218dc015bc4d5e59bf51db2f2cd12b7be4f55',
27
- };
28
- // Map Linux kernel `uname -m` output to a TtydArch key. Returns null for
29
- // architectures we have not pinned a binary for — the caller must treat this
30
- // as a hard error (no silent fallback, no "try latest").
31
- export function mapUnameToTtydArch(uname) {
32
- const trimmed = uname.trim();
33
- if (trimmed === 'aarch64' || trimmed === 'arm64')
34
- return 'aarch64';
35
- if (trimmed === 'armv7l' || trimmed === 'armv6l' || trimmed === 'arm')
36
- return 'arm';
37
- if (trimmed === 'x86_64' || trimmed === 'amd64')
38
- return 'x86_64';
39
- return null;
40
- }
41
- export function ttydDownloadUrl(arch) {
42
- return `https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/${TTYD_ASSET_BY_ARCH[arch]}`;
43
- }
3
+ // Task 664 removed the ttyd pin along with the embedded-terminal admin
4
+ // stack. If a future installer step needs to download a pinned binary
5
+ // (as ttyd did, because Debian Bookworm's apt does not carry it), the
6
+ // conventions live here: version constant, SHA256-by-arch map, and a
7
+ // `mapUnameToArch` helper plus a `downloadUrl` builder. SHA256 mismatch
8
+ // must abort with a loud error and never write bytes — no silent
9
+ // install of unverified content. This file is currently empty of
10
+ // entries; it is kept as the canonical location for when the next
11
+ // upstream-pinned binary is added.
12
+ export {};
package/dist/uninstall.js CHANGED
@@ -85,14 +85,23 @@ export function isMaxyInstalled() {
85
85
  * present — its runtime still depends on those singletons.
86
86
  *
87
87
  * Detection: any `.service` file in `~/.config/systemd/user/` whose name is
88
- * not this brand's `BRAND.serviceName`. This mirrors the install-time check
89
- * at index.ts:489 which uses the same signal for apt/hostname behavior. */
88
+ * not this brand's `BRAND.serviceName` and not this brand's own edge unit
89
+ * (Task 662 each brand owns its own per-brand edge unit, so that file
90
+ * must not register as peer evidence). Task 664 retired the per-brand
91
+ * ttyd unit, so it no longer needs an exclusion.
92
+ *
93
+ * Legacy pre-662 shared `maxy-edge.service` (and pre-664 `maxy-ttyd.service`)
94
+ * are intentionally counted as peer evidence on uninstall: on a dual-brand
95
+ * device the legacy shared unit belonged to whichever brand last ran the
96
+ * installer — treating it as peer evidence keeps the uninstaller from
97
+ * purging device-wide singletons the other brand still depends on. */
90
98
  function peerBrandPresent() {
91
99
  const systemdUserDir = resolve(HOME, ".config/systemd/user");
92
100
  if (!existsSync(systemdUserDir))
93
101
  return false;
102
+ const thisBrandEdge = `${BRAND.hostname}-edge.service`;
94
103
  try {
95
- return readdirSync(systemdUserDir).some((f) => f.endsWith(".service") && f !== BRAND.serviceName && !f.startsWith("maxy-edge") && !f.startsWith("maxy-ttyd"));
104
+ return readdirSync(systemdUserDir).some((f) => f.endsWith(".service") && f !== BRAND.serviceName && f !== thisBrandEdge);
96
105
  }
97
106
  catch {
98
107
  return false;
@@ -111,15 +120,26 @@ function stopServices() {
111
120
  catch {
112
121
  console.log(` ${BRAND.serviceName} not running`);
113
122
  }
114
- // Task 647: maxy-edge owns the public port and VNC stack. Stop it after
115
- // maxy-ui so the edge's ExecStopPost (vnc.sh stop) runs cleanly.
123
+ // Task 647: the edge service owns the public port and VNC stack. Stop it
124
+ // after the main brand service so the edge's ExecStopPost (vnc.sh stop)
125
+ // runs cleanly. Task 662: the edge unit is per-brand (`<hostname>-edge`).
126
+ // Task 664 retired the ttyd unit; pre-664 devices may still have
127
+ // `<hostname>-ttyd` — best-effort stop so the uninstall is clean on
128
+ // upgraded devices too.
129
+ const edgeUnitShort = `${BRAND.hostname}-edge`;
130
+ const legacyTtydUnitShort = `${BRAND.hostname}-ttyd`;
116
131
  try {
117
- spawnSync("systemctl", ["--user", "stop", "maxy-edge"], { stdio: "pipe", timeout: 15_000 });
118
- console.log(" Stopped maxy-edge");
132
+ spawnSync("systemctl", ["--user", "stop", edgeUnitShort], { stdio: "pipe", timeout: 15_000 });
133
+ console.log(` Stopped ${edgeUnitShort}`);
119
134
  }
120
135
  catch {
121
- console.log(" maxy-edge not running");
136
+ console.log(` ${edgeUnitShort} not running`);
137
+ }
138
+ try {
139
+ spawnSync("systemctl", ["--user", "stop", legacyTtydUnitShort], { stdio: "pipe", timeout: 15_000 });
140
+ console.log(` Stopped ${legacyTtydUnitShort} (legacy pre-664 unit)`);
122
141
  }
142
+ catch { /* not present on post-664 installs */ }
123
143
  // Stop Neo4j — dedicated branded instance if this brand uses one, else shared.
124
144
  // Brand isolation (Task 659): never stop `neo4j.service` when this brand runs
125
145
  // a dedicated `neo4j-<hostname>.service` — stopping the shared instance would
@@ -551,19 +571,27 @@ function removeSystemdService() {
551
571
  console.log(` Failed to remove service file: ${err instanceof Error ? err.message : String(err)}`);
552
572
  }
553
573
  }
554
- // Task 647: remove maxy-edge.service alongside maxy-ui.
555
- try {
556
- spawnSync("systemctl", ["--user", "disable", "maxy-edge"], { stdio: "pipe" });
557
- }
558
- catch { /* ignore */ }
559
- const edgeServiceFile = resolve(HOME, ".config/systemd/user/maxy-edge.service");
560
- if (existsSync(edgeServiceFile)) {
574
+ // Task 647 + 662: remove this brand's per-brand edge unit alongside the
575
+ // main brand unit. Task 664 retired the per-brand ttyd unit, but an
576
+ // upgraded device may still have `<hostname>-ttyd.service` on disk from
577
+ // a pre-664 install — cleanup both so the uninstall leaves nothing
578
+ // behind on either generation.
579
+ const thisBrandEdge = `${BRAND.hostname}-edge`;
580
+ const legacyBrandTtyd = `${BRAND.hostname}-ttyd`;
581
+ for (const unit of [thisBrandEdge, legacyBrandTtyd]) {
561
582
  try {
562
- rmSync(edgeServiceFile);
563
- console.log(" Removed maxy-edge.service");
583
+ spawnSync("systemctl", ["--user", "disable", unit], { stdio: "pipe" });
564
584
  }
565
- catch (err) {
566
- console.log(` Failed to remove maxy-edge.service: ${err instanceof Error ? err.message : String(err)}`);
585
+ catch { /* ignore */ }
586
+ const unitFile = resolve(HOME, `.config/systemd/user/${unit}.service`);
587
+ if (existsSync(unitFile)) {
588
+ try {
589
+ rmSync(unitFile);
590
+ console.log(` Removed ${unit}.service`);
591
+ }
592
+ catch (err) {
593
+ console.log(` Failed to remove ${unit}.service: ${err instanceof Error ? err.message : String(err)}`);
594
+ }
567
595
  }
568
596
  }
569
597
  // Reload daemon
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.684",
3
+ "version": "1.0.686",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -7,6 +7,7 @@
7
7
  "tagline": "Built for agents. By agents.",
8
8
  "domain": "realagent.network",
9
9
  "neo4jPort": 7688,
10
+ "ttydPort": 7682,
10
11
 
11
12
  "defaultColors": {
12
13
  "primary": "#7C8C72",
@@ -79,16 +79,19 @@ mkdir -p "${CFG_DIR}"
79
79
  if [ ! -f "${CFG_DIR}/cert.pem" ]; then
80
80
  phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem" display="${DISPLAY:-:99}"
81
81
 
82
- # CDP precheck — fail loudly if Chromium DevTools is not answering.
82
+ # CDP precheck — if Chromium DevTools is answering, we'll drive the
83
+ # Authorize click for the operator. If it isn't (action runner on a
84
+ # device without VNC — Task 664), we fall back to the ActionLogPanel
85
+ # URL-to-button flow: extract the URL, emit it as `OAUTH_URL: <url>`,
86
+ # and wait for cert.pem to land from the operator's own browser click.
87
+ CDP_AVAILABLE=1
83
88
  if ! curl -sf --max-time 2 "http://127.0.0.1:9222/json/version" > /dev/null 2>&1; then
84
- phase_line setup-tunnel step=oauth-login result=error reason=cdp-unreachable \
85
- endpoint=http://127.0.0.1:9222 hint="run ~/vnc.sh restart"
86
- echo "ERROR: Chromium CDP on 127.0.0.1:9222 is not reachable." >&2
87
- echo " The script needs CDP to drive the authorize URL on the VNC browser." >&2
88
- echo " Fix: run 'vnc.sh restart' to bring Chromium up on :99." >&2
89
- exit 1
89
+ phase_line setup-tunnel step=oauth-login cdp=unavailable \
90
+ endpoint=http://127.0.0.1:9222 mode=operator-browser-fallback
91
+ CDP_AVAILABLE=0
92
+ else
93
+ phase_line setup-tunnel step=oauth-login cdp=ok
90
94
  fi
91
- phase_line setup-tunnel step=oauth-login cdp=ok
92
95
 
93
96
  URL_FILE="$(mktemp -t maxy-setup-tunnel-url.XXXXXX)"
94
97
  LAST_LINE_FILE="$(mktemp -t maxy-setup-tunnel-last.XXXXXX)"
@@ -154,6 +157,22 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
154
157
  AUTH_URL="$(cat "${URL_FILE}")"
155
158
  phase_line setup-tunnel step=browser-drive url_extracted=1
156
159
 
160
+ # Emit the URL on stdout in a shape ActionLogPanel's regex captures.
161
+ # Structured-looking enough to route to the "Authorise in Cloudflare"
162
+ # button on Task 664's admin surface without interfering with the
163
+ # existing cloudflared-line extraction (which scans the same URL on
164
+ # stderr from the line above). Emitted before CDP so the button is
165
+ # available even when CDP succeeds — same-URL clicks are idempotent.
166
+ printf 'OAUTH_URL: %s\n' "${AUTH_URL}"
167
+
168
+ # If CDP isn't available on this device, skip the auto-click branch
169
+ # and fall through to the cert.pem poll. The operator's own browser
170
+ # click (via ActionLogPanel's button) hits the same webhook; the
171
+ # callback writes ~/.cloudflared/cert.pem the moment consent lands,
172
+ # and the existing LOGIN_TIMEOUT loop below picks it up.
173
+ if [ "${CDP_AVAILABLE}" -eq 0 ]; then
174
+ phase_line setup-tunnel step=browser-drive mode=operator-click url="${AUTH_URL}"
175
+ else
157
176
  # Drive CDP. Same PUT /json/new?<url> contract as
158
177
  # platform/ui/app/lib/cdp-client.ts (which uses encodeURIComponent on
159
178
  # the URL). Without percent-encoding, CDP's URL parser splits on the
@@ -227,6 +246,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
227
246
  echo " close the sign-in tab, and re-run setup — or run ~/reset-tunnel.sh first." >&2
228
247
  exit 1
229
248
  fi
249
+ fi # end CDP-available branch (Task 664)
230
250
 
231
251
  # Wait for cert.pem to land — cloudflared writes to ~/.cloudflared/cert.pem
232
252
  # regardless of --origincert, so watch the canonical location. Task 588
@@ -235,7 +255,13 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
235
255
  # 2-second heartbeat inside the loop is the observability contract for
236
256
  # this bounded wait — no form-spawned script is allowed a silent poll of
237
257
  # more than ~2 s per criterion 3 of Task 588.
238
- LOGIN_TIMEOUT="${SETUP_TUNNEL_LOGIN_TIMEOUT:-20}"
258
+ # Task 664: operator-click path (no CDP) needs a human-paced window;
259
+ # CDP auto-click stays on the 20s round-trip budget.
260
+ if [ "${CDP_AVAILABLE}" -eq 0 ]; then
261
+ LOGIN_TIMEOUT="${SETUP_TUNNEL_LOGIN_TIMEOUT:-180}"
262
+ else
263
+ LOGIN_TIMEOUT="${SETUP_TUNNEL_LOGIN_TIMEOUT:-20}"
264
+ fi
239
265
  LOGIN_WAIT=0
240
266
  while [ ! -f "${HOME}/.cloudflared/cert.pem" ]; do
241
267
  if ! kill -0 "${CF_PIPELINE_PID}" 2>/dev/null; then
@@ -17,6 +17,7 @@ Load these when users ask about Maxy features or need guidance:
17
17
  - **Getting started** → `references/getting-started.md` — first run, what Maxy is, how to use it
18
18
  - **Plugins** → `references/plugins-guide.md` — what plugins are, how to install/remove them, the marketplace
19
19
  - **Memory** → `references/memory-guide.md` — how Maxy remembers things, what is stored, privacy
20
+ - **Graph view** → `references/graph.md` — the admin `/graph` page: node/edge display, zoom-adaptive Conversation labels, filter chips, trashed nodes
20
21
  - **Contacts** → `references/contacts-guide.md` — adding, looking up, and managing contacts
21
22
  - **Telegram** → `references/telegram-guide.md` — Telegram setup, the bot, daily use
22
23
  - **Settings** → `references/settings.md` — output style, effort level, context mode, account preferences
@@ -38,6 +39,7 @@ Load these when performing admin tasks or diagnosing platform behaviour:
38
39
  - references/getting-started.md
39
40
  - references/plugins-guide.md
40
41
  - references/memory-guide.md
42
+ - references/graph.md
41
43
  - references/contacts-guide.md
42
44
  - references/telegram-guide.md
43
45
  - references/settings.md
@@ -22,7 +22,7 @@ Ask the agent to set up Cloudflare. The agent first confirms the domain is alrea
22
22
  - **Proxy apex** — optional bare-domain hostname (e.g. `yourdomain.com`) that should also serve the public agent.
23
23
  - **Admin password** — the password used to gate remote access to the admin surface.
24
24
 
25
- When you submit, the `/api/admin/cloudflare/setup` endpoint runs — in strict order — `setRemotePassword`, `setup-tunnel.sh`, and alias-domain writes for every non-`public.*` public or apex hostname (so e.g. `chat.yourdomain.com` is classified as public by `isPublicHost()`). The script runs end-to-end:
25
+ When you submit, the `/api/admin/cloudflare/setup` endpoint runs — in strict order — `setRemotePassword`, launches a `cloudflare-setup` action (Task 664: `systemd-run --user` transient unit wrapping `setup-tunnel.sh <brand> <port> <hostname...>`), and registers a post-exit handler to write alias-domains for every non-`public.*` public or apex hostname (so e.g. `chat.yourdomain.com` is classified as public by `isPublicHost()`). The script runs end-to-end:
26
26
 
27
27
  - `cloudflared tunnel login` — OAuth browser sign-in. The VNC browser opens the Cloudflare authorize page; pick the account that owns your domain, click Authorize. `cert.pem` lands.
28
28
  - Tunnel creation under the naming convention `{brand}-{hostname}` (e.g. `maxy-neo`). Stream log emits `step=tunnel-resolve action=reused|created` once the UUID is known so the admin agent can see which tunnel the later steps will write against.