@rubytech/create-realagent 1.0.685 → 1.0.687

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 (35) hide show
  1. package/dist/index.js +23 -215
  2. package/dist/pinned-binaries.js +10 -41
  3. package/dist/uninstall.js +23 -23
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +35 -9
  6. package/payload/platform/plugins/docs/PLUGIN.md +2 -0
  7. package/payload/platform/plugins/docs/references/cloudflare.md +1 -1
  8. package/payload/platform/plugins/docs/references/deployment.md +13 -10
  9. package/payload/platform/plugins/docs/references/graph.md +38 -0
  10. package/payload/platform/plugins/docs/references/platform.md +10 -7
  11. package/payload/platform/plugins/docs/references/troubleshooting.md +23 -13
  12. package/payload/platform/scripts/vnc.sh +7 -7
  13. package/payload/platform/templates/systemd/edge.service.template +5 -4
  14. package/payload/server/maxy-edge.js +5 -367
  15. package/payload/server/public/assets/admin-BqLtaMVu.js +352 -0
  16. package/payload/server/public/assets/{data-DUSyrydY.js → data-BZ7v-zug.js} +1 -1
  17. package/payload/server/public/assets/{file-CDJ6dUV3.js → file-CScYkZq5.js} +1 -1
  18. package/payload/server/public/assets/graph-tjXdtwk-.js +50 -0
  19. package/payload/server/public/assets/{house-CNP_bwvT.js → house-CdFRNujU.js} +1 -1
  20. package/payload/server/public/assets/{jsx-runtime-BFFQvkdQ.css → jsx-runtime-Og0q7dXg.css} +1 -1
  21. package/payload/server/public/assets/{public-sHoAccvb.js → public-CrkQJek6.js} +2 -2
  22. package/payload/server/public/assets/{share-2-DBcb9j6E.js → share-2-Ev-D4Lm9.js} +1 -1
  23. package/payload/server/public/assets/{useVoiceRecorder-CtSgpc95.js → useVoiceRecorder-DyDXH7EA.js} +2 -2
  24. package/payload/server/public/assets/{x-CTVJaC_u.js → x-D5W7ddgP.js} +1 -1
  25. package/payload/server/public/data.html +6 -6
  26. package/payload/server/public/graph.html +6 -6
  27. package/payload/server/public/index.html +7 -8
  28. package/payload/server/public/public.html +4 -4
  29. package/payload/server/server.js +830 -258
  30. package/payload/platform/templates/dotfiles/.tmux.conf +0 -1
  31. package/payload/platform/templates/systemd/ttyd.service.template +0 -30
  32. package/payload/server/public/assets/admin-BFmYXz1V.js +0 -362
  33. package/payload/server/public/assets/admin-kHJ-D0s7.css +0 -1
  34. package/payload/server/public/assets/graph-CWcYp5bE.js +0 -50
  35. /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,188 +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 per-brand ttyd --user unit (Task 662). The unit
1735
- // file name and the loopback port are both brand-scoped so two brands
1736
- // installed on the same device each run their own ttyd process without
1737
- // contending for either the filesystem path or the TCP port:
1738
- // `${BRAND.hostname}-ttyd.service` + 127.0.0.1:${BRAND.ttydPort}
1739
- const systemdUserDir = resolve(homeDir, ".config/systemd/user");
1740
- mkdirSync(systemdUserDir, { recursive: true });
1741
- const ttydUnitShort = `${BRAND.hostname}-ttyd`;
1742
- const ttydUnitName = `${ttydUnitShort}.service`;
1743
- // Skip systemd-unit install if the ttyd binary is not in place — enabling
1744
- // a unit whose ExecStart points at a missing file just churns systemd with
1745
- // restart failures.
1746
- if (!ttydReady) {
1747
- console.error(` Skipping ${ttydUnitName} install — ttyd binary not present. Admin terminal will be unavailable until remediated.`);
1748
- return;
1749
- }
1750
- const ttydUnitTemplate = resolve(INSTALL_DIR, "platform/templates/systemd/ttyd.service.template");
1751
- const ttydUnitDest = join(systemdUserDir, ttydUnitName);
1752
- try {
1753
- if (existsSync(ttydUnitTemplate)) {
1754
- const ttydServiceContent = readFileSync(ttydUnitTemplate, "utf-8")
1755
- .replace(/__TTYD_PORT__/g, String(TTYD_PORT));
1756
- writeFileSync(ttydUnitDest, ttydServiceContent);
1757
- logFile(` ${ttydUnitName}: TTYD_PORT=${TTYD_PORT}`);
1758
- }
1759
- else {
1760
- console.error(` WARNING: ttyd.service.template missing at ${ttydUnitTemplate} — admin terminal will not work`);
1761
- return;
1762
- }
1763
- }
1764
- catch (err) {
1765
- console.error(` WARNING: failed to write ${ttydUnitDest}: ${err instanceof Error ? err.message : String(err)}`);
1766
- return;
1767
- }
1768
- spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
1769
- spawnSync("systemctl", ["--user", "enable", ttydUnitShort], { stdio: "inherit" });
1770
- spawnSync("systemctl", ["--user", "restart", ttydUnitShort], { stdio: "inherit" });
1771
- console.log(` ${ttydUnitName} enabled — admin terminal available on 127.0.0.1:${TTYD_PORT}`);
1772
- }
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.
1773
1600
  function installService() {
1774
- log("12", TOTAL, `Starting ${BRAND.productName}...`);
1601
+ log("11", TOTAL, `Starting ${BRAND.productName}...`);
1775
1602
  if (!isLinux()) {
1776
1603
  console.log(" Skipping systemd service (not Linux). Start manually with:");
1777
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`);
@@ -1904,13 +1731,12 @@ WantedBy=default.target
1904
1731
  .replace(/__INSTALL_DIR__/g, INSTALL_DIR)
1905
1732
  .replace(/__EDGE_PORT__/g, String(PORT))
1906
1733
  .replace(/__MAXY_UI_PORT__/g, String(MAXY_UI_INTERNAL_PORT))
1907
- .replace(/__TTYD_PORT__/g, String(TTYD_PORT))
1908
1734
  .replace(/__PERSIST_DIR__/g, persistDir);
1909
1735
  writeFileSync(join(serviceDir, edgeUnitName), edgeServiceContent);
1910
- logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT} TTYD_PORT=${TTYD_PORT}`);
1736
+ logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT}`);
1911
1737
  }
1912
1738
  else {
1913
- console.error(` WARNING: 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`);
1914
1740
  }
1915
1741
  // Task 560: the unit declares Environment=PATH=%h/.local/bin:... so the graph
1916
1742
  // MCP shim's spawn("uvx", ...) resolves against uv's install location. Without
@@ -2328,27 +2154,11 @@ else {
2328
2154
  // Dedicated = port differs from the default shared instance
2329
2155
  const NEO4J_DEDICATED = NEO4J_PORT !== DEFAULT_NEO4J_PORT;
2330
2156
  // ---------------------------------------------------------------------------
2331
- // TTYD port per-brand loopback so two brands on the same device each run
2332
- // their own ttyd unit on a distinct port (Task 662). Edge service proxies
2333
- // /ttyd to this port via Environment=TTYD_PORT.
2334
- //
2335
- // Priority: --ttyd-port flag > BRAND.ttydPort > 7681. Same pattern as
2336
- // NEO4J_PORT (Task 659).
2337
- // ---------------------------------------------------------------------------
2338
- const DEFAULT_TTYD_PORT = 7681;
2339
- let TTYD_PORT = BRAND.ttydPort ?? DEFAULT_TTYD_PORT;
2340
- let TTYD_PORT_SOURCE = BRAND.ttydPort ? "brand.json" : "default";
2341
- const ttydPortIdx = _args.indexOf("--ttyd-port");
2342
- if (ttydPortIdx !== -1) {
2343
- const raw = _args[ttydPortIdx + 1];
2344
- const parsed = raw ? parseInt(raw, 10) : NaN;
2345
- if (isNaN(parsed) || parsed < 1024 || parsed > 65535) {
2346
- console.error(`Setup failed: --ttyd-port requires a numeric value between 1024 and 65535 (got: ${raw ?? "nothing"})`);
2347
- process.exit(1);
2348
- }
2349
- TTYD_PORT = parsed;
2350
- TTYD_PORT_SOURCE = "--ttyd-port flag";
2351
- }
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.
2352
2162
  const PKG_VERSION = JSON.parse(readFileSync(resolve(import.meta.dirname, "../package.json"), "utf-8")).version;
2353
2163
  initLogging();
2354
2164
  console.log("================================================================");
@@ -2361,7 +2171,6 @@ if (HOSTNAME_FLAG)
2361
2171
  console.log(` Display: ${DISPLAY_MODE} (${DISPLAY_MODE_SOURCE})`);
2362
2172
  console.log(` Embed model: ${EMBED_MODEL} (${EMBED_DIMS} dims, ${EMBED_SOURCE})`);
2363
2173
  console.log(` Neo4j: ${NEO4J_DEDICATED ? "dedicated" : "shared"} on bolt://localhost:${NEO4J_PORT} (${NEO4J_PORT_SOURCE})`);
2364
- console.log(` ttyd: 127.0.0.1:${TTYD_PORT} (${TTYD_PORT_SOURCE})`);
2365
2174
  console.log("");
2366
2175
  logDiagnostics("pre-flight");
2367
2176
  logFile(` Neo4j instance: ${NEO4J_DEDICATED ? "dedicated" : "shared"} on bolt://localhost:${NEO4J_PORT}`);
@@ -2382,7 +2191,6 @@ try {
2382
2191
  setupVncViewer();
2383
2192
  setupAccount();
2384
2193
  installTunnelScripts(); // ~/setup-tunnel.sh, ~/reset-tunnel.sh — the SKILL contract
2385
- installTerminalService(); // Task 657: installs maxy-ttyd.service (ttyd + tmux) for byte-stream admin terminal
2386
2194
  installService();
2387
2195
  console.log("");
2388
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,13 +85,14 @@ 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` and not this brand's own edge/ttyd
89
- * unit (Task 662 — each brand owns its own per-brand edge + ttyd unit, so
90
- * those files must not register as peer evidence).
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.
91
92
  *
92
- * Legacy pre-662 shared `maxy-edge.service` / `maxy-ttyd.service` are
93
- * intentionally counted as peer evidence on uninstall: on a dual-brand
94
- * pre-662 device the shared unit belonged to whichever brand last ran the
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
95
96
  * installer — treating it as peer evidence keeps the uninstaller from
96
97
  * purging device-wide singletons the other brand still depends on. */
97
98
  function peerBrandPresent() {
@@ -99,9 +100,8 @@ function peerBrandPresent() {
99
100
  if (!existsSync(systemdUserDir))
100
101
  return false;
101
102
  const thisBrandEdge = `${BRAND.hostname}-edge.service`;
102
- const thisBrandTtyd = `${BRAND.hostname}-ttyd.service`;
103
103
  try {
104
- return readdirSync(systemdUserDir).some((f) => f.endsWith(".service") && f !== BRAND.serviceName && f !== thisBrandEdge && f !== thisBrandTtyd);
104
+ return readdirSync(systemdUserDir).some((f) => f.endsWith(".service") && f !== BRAND.serviceName && f !== thisBrandEdge);
105
105
  }
106
106
  catch {
107
107
  return false;
@@ -122,10 +122,12 @@ function stopServices() {
122
122
  }
123
123
  // Task 647: the edge service owns the public port and VNC stack. Stop it
124
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
- // plus the ttyd unit (`<hostname>-ttyd`) both must be stopped.
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.
127
129
  const edgeUnitShort = `${BRAND.hostname}-edge`;
128
- const ttydUnitShort = `${BRAND.hostname}-ttyd`;
130
+ const legacyTtydUnitShort = `${BRAND.hostname}-ttyd`;
129
131
  try {
130
132
  spawnSync("systemctl", ["--user", "stop", edgeUnitShort], { stdio: "pipe", timeout: 15_000 });
131
133
  console.log(` Stopped ${edgeUnitShort}`);
@@ -134,12 +136,10 @@ function stopServices() {
134
136
  console.log(` ${edgeUnitShort} not running`);
135
137
  }
136
138
  try {
137
- spawnSync("systemctl", ["--user", "stop", ttydUnitShort], { stdio: "pipe", timeout: 15_000 });
138
- console.log(` Stopped ${ttydUnitShort}`);
139
- }
140
- catch {
141
- console.log(` ${ttydUnitShort} not running`);
139
+ spawnSync("systemctl", ["--user", "stop", legacyTtydUnitShort], { stdio: "pipe", timeout: 15_000 });
140
+ console.log(` Stopped ${legacyTtydUnitShort} (legacy pre-664 unit)`);
142
141
  }
142
+ catch { /* not present on post-664 installs */ }
143
143
  // Stop Neo4j — dedicated branded instance if this brand uses one, else shared.
144
144
  // Brand isolation (Task 659): never stop `neo4j.service` when this brand runs
145
145
  // a dedicated `neo4j-<hostname>.service` — stopping the shared instance would
@@ -571,14 +571,14 @@ function removeSystemdService() {
571
571
  console.log(` Failed to remove service file: ${err instanceof Error ? err.message : String(err)}`);
572
572
  }
573
573
  }
574
- // Task 647 + 662: remove this brand's per-brand edge + ttyd units alongside
575
- // the main brand unit. Peer brand's edge/ttyd units live at different
576
- // filenames (`<peer-hostname>-edge.service`) and are never touched here.
577
- // Legacy pre-662 shared units left on disk by a previous installer are
578
- // cleaned up by the installer pre-hygiene loop, not here.
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
579
  const thisBrandEdge = `${BRAND.hostname}-edge`;
580
- const thisBrandTtyd = `${BRAND.hostname}-ttyd`;
581
- for (const unit of [thisBrandEdge, thisBrandTtyd]) {
580
+ const legacyBrandTtyd = `${BRAND.hostname}-ttyd`;
581
+ for (const unit of [thisBrandEdge, legacyBrandTtyd]) {
582
582
  try {
583
583
  spawnSync("systemctl", ["--user", "disable", unit], { stdio: "pipe" });
584
584
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.685",
3
+ "version": "1.0.687",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -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.
@@ -68,21 +68,24 @@ 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 installed brand runs three per-brand `--user` systemd units (Task 662 — unit filenames are prefixed with the brand's `hostname` so two brands on the same device never share a unit file):
71
+ Each installed brand runs two per-brand `--user` systemd units (Task 662 + Task 664 — unit filenames are prefixed with the brand's `hostname` so two brands on the same device never share a unit file):
72
72
 
73
- - `{hostname}.service` — the admin + public HTTP server on `127.0.0.1:19199`. Restarted by the upgrade flow; short downtime is expected during steps 8→12 of an upgrade.
74
- - `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service, handles `/websockify` (VNC) and `/ttyd` (admin terminal) WebSocket upgrades locally. Does NOT restart during an upgrade — the browser WebSocket stays connected by construction.
75
- - `{hostname}-ttyd.service` — `ttyd` bound to `127.0.0.1:{BRAND.ttydPort}` (7681 Maxy, 7682 Real Agent), running `tmux new-session -A -s maxy-pty`. Owns the byte-stream admin terminal rendered by xterm.js in the header overlay and the Software Update modal (Task 657). Independent of the main brand service and the edge; outlives service restarts so scrollback is preserved.
73
+ - `{hostname}.service` — the admin + public HTTP server on `127.0.0.1:19199`. Restarted by the upgrade flow; short downtime is expected during steps 8→11 of an upgrade.
74
+ - `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service and handles `/websockify` (VNC) WebSocket upgrades locally. Does NOT restart during an upgrade — the browser WebSocket stays connected by construction.
76
75
 
77
- If the admin terminal fails to open, check `sudo tail -n 50 ~/{configDir}/logs/edge-boot.log` the `ttyd-ws-upgrade` / `ttyd-proxy-open` / `ttyd-proxy-close` lines carry a `corrId` that ties the full session lifecycle together. For unit health, `systemctl --user status {hostname}-ttyd` + `journalctl --user -u {hostname}-ttyd`.
76
+ Upgrade and Cloudflare setup (Task 664) run as detached actions: `systemd-run --user` transient units per invocation with stdout+stderr persisted to `~/.maxy/logs/actions/<actionId>.log` and streamed to the UI via SSE. No boot-time service file exists for these.
78
77
 
79
- **Pre-Task-662 upgrade** — devices that ran an installer before Task 662 have legacy shared `maxy-edge.service` and `maxy-ttyd.service` units on disk, which will collide with the new per-brand units. Before re-running any installer on such a device:
78
+ If an action looks stuck, read `~/.maxy/logs/actions/<actionId>.log` directly for the full output, or `journalctl --user --identifier=maxy-action-<actionId>` for systemd's record.
79
+
80
+ **Pre-Task-662 / pre-Task-664 upgrade** — devices that ran an installer before Task 662 have legacy shared `maxy-edge.service` / `maxy-ttyd.service` units; devices that ran before Task 664 have per-brand `{hostname}-ttyd.service` units plus a pinned `/usr/local/bin/ttyd` binary. Neither is removed automatically — do this cleanup once per device before re-running any installer:
80
81
 
81
82
  ```bash
82
- systemctl --user stop maxy-edge maxy-ttyd 2>/dev/null || true
83
- systemctl --user disable maxy-edge maxy-ttyd 2>/dev/null || true
83
+ systemctl --user stop maxy-edge maxy-ttyd realagent-ttyd 2>/dev/null || true
84
+ systemctl --user disable maxy-edge maxy-ttyd realagent-ttyd 2>/dev/null || true
84
85
  rm -f ~/.config/systemd/user/maxy-edge.service \
85
- ~/.config/systemd/user/maxy-ttyd.service
86
+ ~/.config/systemd/user/maxy-ttyd.service \
87
+ ~/.config/systemd/user/realagent-ttyd.service
88
+ sudo rm -f /usr/local/bin/ttyd
86
89
  systemctl --user daemon-reload
87
90
  ```
88
91
 
@@ -90,7 +93,7 @@ systemctl --user daemon-reload
90
93
 
91
94
  A single Pi or laptop can host more than one brand (for example Maxy and Real Agent) side by side. Each brand runs as its own service on its own port, with its own install directory and its own data. Installing one brand does not touch the other.
92
95
 
93
- - **Separate:** each brand has its own install folder (`~/maxy/`, `~/realagent/`), its own config folder (`~/.maxy/`, `~/.realagent/`), its own web port, its own Cloudflare tunnel state, its own edge + ttyd systemd units (`maxy-edge.service` + `maxy-ttyd.service` vs `realagent-edge.service` + `realagent-ttyd.service`), its own ttyd loopback port (7681 vs 7682), and by default its own Neo4j database (Maxy on bolt port 7687, Real Agent on 7688).
96
+ - **Separate:** each brand has its own install folder (`~/maxy/`, `~/realagent/`), its own config folder (`~/.maxy/`, `~/.realagent/`), its own web port, its own Cloudflare tunnel state, its own edge systemd unit (`maxy-edge.service` vs `realagent-edge.service`), and by default its own Neo4j database (Maxy on bolt port 7687, Real Agent on 7688). Action runner units are transient and per-invocation, not per-brand, so no naming conflict is possible.
94
97
  - **Shared:** both brands share the system Chromium/VNC stack, the Ollama model server, and the `cloudflared` command itself. Browser automation is serialised — one admin session at a time across both brands.
95
98
 
96
99
  To install a second brand on a device that already runs the first, just run the other installer. No flags needed for isolation: