#!/usr/bin/env bash # # Hyperspace Network — Binary Install Script # # Downloads a pre-compiled binary — no Node.js, pnpm, or git required. # On desktop environments (macOS, Linux with display), also installs the # system tray app with auto-update and crash recovery. # # Usage: # curl -fsSL https://agents.hyper.space/cli | bash # # or # bash install.sh [--no-start] [--key ] # # Options: # --no-start Don't auto-start the node after install # --no-tray Skip tray app installation on desktop # --sudo Use sudo to install to /usr/local/bin (fixes PATH issues) # --key Use existing Ed25519 private key # --api-port N OpenAI API server port (default: 8080) # # Environment: # HYPERSPACE_PRIVATE_KEY Ed25519 private key (alternative to --key) # set -euo pipefail # ─── Colors ─── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' info() { echo -e "${BLUE}[*]${NC} $1"; } ok() { echo -e "${GREEN}[+]${NC} $1"; } warn() { echo -e "${YELLOW}[!]${NC} $1"; } err() { echo -e "${RED}[-]${NC} $1"; } step() { echo -e "${CYAN}==>${NC} ${BOLD}$1${NC}"; } # ─── Parse args ─── NO_START=false NO_TRAY=false USE_SUDO=false PRIVATE_KEY="" API_PORT=8080 # One-click LAN join: `--join ` (or HYPERSPACE_JOIN env) auto-joins # the trusted LAN pod after install. Also accepts a full /join/ URL. JOIN_TOKEN="${HYPERSPACE_JOIN:-}" while [[ $# -gt 0 ]]; do case $1 in --no-start) NO_START=true; shift ;; --no-tray) NO_TRAY=true; shift ;; --sudo) USE_SUDO=true; shift ;; --key) PRIVATE_KEY="$2"; shift 2 ;; --api-port) API_PORT="$2"; shift 2 ;; --join) JOIN_TOKEN="$2"; shift 2 ;; --join=*) JOIN_TOKEN="${1#--join=}"; shift ;; *) shift ;; esac done # ─── Banner ─── echo "" echo -e "${BOLD}┌─────────────────────────────────────────────────────────┐${NC}" echo -e "${BOLD}│ hyperspace │${NC}" echo -e "${BOLD}│ network of agents │${NC}" echo -e "${BOLD}└─────────────────────────────────────────────────────────┘${NC}" echo "" # ─── Detect system ─── step "Detecting system..." OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) # Map to GitHub release asset names case "$OS-$ARCH" in linux-x86_64) ASSET_NAME="aios-cli-x86_64-unknown-linux-gnu.tar.gz" ;; linux-aarch64|linux-arm64) # Cross-built on every release (build-release-tarballs.sh PLATFORMS); # the installer refusing this arch was a gap, not a policy. ASSET_NAME="aios-cli-aarch64-unknown-linux-gnu.tar.gz" ;; darwin-arm64) ASSET_NAME="aios-cli-aarch64-apple-darwin.tar.gz" ;; darwin-x86_64) ASSET_NAME="aios-cli-x86_64-apple-darwin.tar.gz" ;; *) err "Unsupported platform: $OS $ARCH" err "Supported: Linux x86_64, Linux ARM64, macOS ARM64, macOS x86_64" exit 1 ;; esac ok "OS: $OS ($ARCH)" # ─── Detect desktop environment ─── IS_DESKTOP=false if [[ "$OS" == "darwin" ]]; then # macOS is always a desktop environment IS_DESKTOP=true elif [[ "$OS" == "linux" ]]; then # Check for display server (X11 or Wayland) if [[ -n "${DISPLAY:-}" ]] || [[ -n "${WAYLAND_DISPLAY:-}" ]]; then IS_DESKTOP=true elif [[ "${XDG_SESSION_TYPE:-}" == "x11" ]] || [[ "${XDG_SESSION_TYPE:-}" == "wayland" ]]; then IS_DESKTOP=true fi fi if [[ "$IS_DESKTOP" == "true" ]]; then ok "Desktop environment detected" else info "Headless server detected (CLI-only install)" fi # Check for GPU if command -v nvidia-smi &>/dev/null; then GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1) GPU_VRAM=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | head -1) ok "GPU: $GPU_NAME (${GPU_VRAM}MB VRAM)" elif command -v rocm-smi &>/dev/null; then ok "GPU: AMD ROCm detected" elif [[ "$OS" == "darwin" ]]; then ok "GPU: Apple Silicon (unified memory)" else warn "No GPU detected — node will participate as relay only" fi # ─── Detect v1 installation ─── V1_MIGRATED=false # Check for running hyperspace/aios-cli processes (v1 or v2) V1_PIDS=$(pgrep -f 'aios-cli|_aios-cli' 2>/dev/null || true) V2_PIDS=$(pgrep -xf '.*hyperspace (start|daemon).*' 2>/dev/null || pgrep -x hyperspace 2>/dev/null || true) if [[ -n "$V1_PIDS" ]]; then warn "Found running v1 aios-cli process(es): $V1_PIDS" warn "Stopping v1 processes to upgrade to v2..." kill $V1_PIDS 2>/dev/null || true sleep 2 kill -9 $V1_PIDS 2>/dev/null || true ok "Stopped v1 processes" V1_MIGRATED=true fi if [[ -n "$V2_PIDS" ]]; then warn "Found running hyperspace process(es): $V2_PIDS" warn "Stopping to upgrade..." kill $V2_PIDS 2>/dev/null || true sleep 2 kill -9 $V2_PIDS 2>/dev/null || true ok "Stopped running hyperspace" fi # Check for v1 key files and inform user they'll be auto-migrated V1_KEY_PATHS=( "${HOME}/.config/hyperspace/key.pem" "${HOME}/Library/Application Support/hyperspace/key.pem" "${HOME}/.hyperspace/key.pem" ) for kp in "${V1_KEY_PATHS[@]}"; do if [[ -f "$kp" ]]; then ok "Found v1 identity at $kp — will auto-migrate on first run" V1_MIGRATED=true break fi done # ─── Install directory ─── INSTALL_DIR="${HOME}/.hyperspace" BIN_DIR="${HOME}/.local/bin" mkdir -p "$INSTALL_DIR" "$BIN_DIR" # Detect shell profile SHELL_RC="${HOME}/.bashrc" [[ -f "${HOME}/.zshrc" ]] && SHELL_RC="${HOME}/.zshrc" # Ensure BIN_DIR is in PATH if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then warn "$BIN_DIR not in PATH — adding to shell profile" echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$SHELL_RC" export PATH="$BIN_DIR:$PATH" fi # Ensure XDG_RUNTIME_DIR is set (required for systemctl --user on headless servers) if [[ "$OS" == "linux" ]] && [[ -z "${XDG_RUNTIME_DIR:-}" ]]; then export XDG_RUNTIME_DIR="/run/user/$(id -u)" if ! grep -q 'XDG_RUNTIME_DIR' "$SHELL_RC" 2>/dev/null; then echo 'export XDG_RUNTIME_DIR="/run/user/$(id -u)"' >> "$SHELL_RC" fi fi # ─── Download binary ─── step "Downloading Hyperspace CLI..." # Download via agents.hyper.space proxy (handles auth for private repo). # # The proxy caches the bare ?asset= ("latest") blob keyed by asset NAME, which # is constant across releases — so it can serve a PRIOR release's tarball even # after a new one publishes (observed: a fresh install pulled the previous # version's python/ shard scripts). The metadata API /api/releases stays fresh, # so resolve the newest tag from it and PIN the download to that tag. An # explicit &tag= forces the proxy to re-resolve that exact release, bypassing # the stale latest-blob cache. LATEST_TAG=$(curl -fsSL --max-time 10 "https://agents.hyper.space/api/releases" 2>/dev/null \ | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | cut -d'"' -f4) if [[ -n "$LATEST_TAG" ]]; then DOWNLOAD_URL="https://agents.hyper.space/api/download?asset=${ASSET_NAME}&tag=${LATEST_TAG}" info "Pinning download to ${LATEST_TAG}" else DOWNLOAD_URL="https://agents.hyper.space/api/download?asset=${ASSET_NAME}" fi # Verify the asset exists before downloading if ! curl -sfI --max-time 10 "$DOWNLOAD_URL" >/dev/null 2>&1; then # Fallback: try public hyperspace-node repo directly FALLBACK_API="https://api.github.com/repos/hyperspaceai/hyperspace-node/releases?per_page=20" DOWNLOAD_URL=$(curl -fsSL "$FALLBACK_API" 2>/dev/null | grep -o "\"browser_download_url\": *\"[^\"]*${ASSET_NAME}\"" | head -1 | cut -d'"' -f4) fi if [[ -z "$DOWNLOAD_URL" ]]; then err "Could not find release asset: $ASSET_NAME" err "Try again later or check https://agents.hyper.space" exit 1 fi info "Downloading from: $DOWNLOAD_URL" TMP_DIR=$(mktemp -d) trap 'rm -rf "$TMP_DIR"' EXIT curl -fSL --progress-bar -o "$TMP_DIR/$ASSET_NAME" "$DOWNLOAD_URL" ok "Download complete" # ─── Extract and install ─── step "Installing CLI..." tar -xzf "$TMP_DIR/$ASSET_NAME" -C "$TMP_DIR" # Install as both 'aios-cli' and 'hyperspace' # Archive contains both 'aios-cli' and '_aios-cli' (v1 compat name) BINARY="" if [[ -f "$TMP_DIR/aios-cli" ]]; then BINARY="$TMP_DIR/aios-cli" elif [[ -f "$TMP_DIR/_aios-cli" ]]; then BINARY="$TMP_DIR/_aios-cli" else err "Binary not found in archive (expected 'aios-cli' or '_aios-cli')" exit 1 fi cp "$BINARY" "$BIN_DIR/aios-cli" chmod +x "$BIN_DIR/aios-cli" cp "$BINARY" "$BIN_DIR/hyperspace" chmod +x "$BIN_DIR/hyperspace" ok "Installed to $BIN_DIR/hyperspace" ok "Installed to $BIN_DIR/aios-cli (v1 compat)" # CRDT WASM — must be next to the binary for loro-crdt to find it if [[ -f "$TMP_DIR/loro_wasm_bg.wasm" ]]; then cp "$TMP_DIR/loro_wasm_bg.wasm" "$BIN_DIR/loro_wasm_bg.wasm" ok "Installed CRDT engine (loro_wasm_bg.wasm)" fi # SQLite native addon — must be next to the binary for better-sqlite3 to find it if [[ -f "$TMP_DIR/better_sqlite3.node" ]]; then cp "$TMP_DIR/better_sqlite3.node" "$BIN_DIR/better_sqlite3.node" ok "Installed SQLite engine (better_sqlite3.node)" fi # Capability census — hyperspace_find/hyperspace_describe (Claude Code MCP) # resolve it next to the binary (manifestSearchPaths). Soft: absence degrades # discovery to an honest hint, never fails the install. if [[ -f "$TMP_DIR/os-capability-manifest.json" ]]; then cp "$TMP_DIR/os-capability-manifest.json" "$BIN_DIR/os-capability-manifest.json" ok "Installed capability census (os-capability-manifest.json)" fi # Sidecar TOML manifests — the daemon's on-boot sidecar supervisor reads # sidecars/*.toml from NEXT TO the binary (defaultSidecarManifestDir probes # dirname(process.execPath)/sidecars). Without these, a curl install boots # ZERO on-boot sidecars (e.g. rusty-browser-sidecar :7099 shipped but never # started). Manifests whose binary is absent are skip-logged at boot, so # seeding the full set is safe. Tolerant: an older tarball without sidecars/ # installs cleanly. if [[ -d "$TMP_DIR/sidecars" ]]; then mkdir -p "$BIN_DIR/sidecars" MANIFESTS_INSTALLED=0 for src in "$TMP_DIR/sidecars/"*.toml; do [[ -f "$src" ]] || continue if cp -f "$src" "$BIN_DIR/sidecars/$(basename "$src")" 2>/dev/null; then MANIFESTS_INSTALLED=$((MANIFESTS_INSTALLED + 1)) fi done [[ $MANIFESTS_INSTALLED -gt 0 ]] && ok "Installed sidecar manifests ($MANIFESTS_INSTALLED — on-boot supervisor)" fi # ORT-Web WASM — search sidecar uses transformers.js + onnxruntime-web for # semantic embeddings. The .wasm + .mjs files must be next to the search # sidecar binary so dynamic imports resolve at runtime. ORT_INSTALLED=0 for f in ort-wasm-simd-threaded.jsep.wasm ort-wasm-simd-threaded.jsep.mjs ort-wasm-simd-threaded.wasm ort-wasm-simd-threaded.mjs; do if [[ -f "$TMP_DIR/$f" ]]; then cp "$TMP_DIR/$f" "$BIN_DIR/$f" ORT_INSTALLED=$((ORT_INSTALLED + 1)) fi done [[ $ORT_INSTALLED -gt 0 ]] && ok "Installed ORT-Web WASM ($ORT_INSTALLED files — semantic search)" # Out-of-process auto-updater — runs every 5min via systemd timer (Linux) # or launchd (macOS), polls agents.hyper.space, signature-verifies the # tarball with a baked-in Ed25519 public key, runs a no-TTY canary, and # atomic-swaps the main daemon's binary. Independent of main daemon's # state — survives crashes, backoff windows, frozen processes. THIS is # what makes auto-update reach 100% reliability across the fleet. # # Two implementations ship in the same release tarball: # - hyperspace-updater[.exe] — Go binary, single static, signature-aware (PRIMARY) # - hyperspace-updater.sh — bash fallback (used if Go binary fails) HSP_BIN_DIR="${HOME}/.hyperspace/bin" mkdir -p "$HSP_BIN_DIR" # Install Go updater binary (primary) GO_UPDATER_NAME="hyperspace-updater" [[ "$OS" == "windows" ]] && GO_UPDATER_NAME="hyperspace-updater.exe" if [[ -f "$TMP_DIR/$GO_UPDATER_NAME" ]]; then cp "$TMP_DIR/$GO_UPDATER_NAME" "$HSP_BIN_DIR/$GO_UPDATER_NAME" chmod +x "$HSP_BIN_DIR/$GO_UPDATER_NAME" ok "Installed out-of-process auto-updater (Go binary, signature-verifying)" fi # Install supervisor binary (v5.46.0+) — staged but NOT activated. # Activated via `hyperspace supervisor install` once you're ready to switch # from the existing daemon plist to the supervisor-managed model. v5.46.1 # will make activation default. if [[ -f "$TMP_DIR/hyperspace-supervisor" ]]; then cp "$TMP_DIR/hyperspace-supervisor" "$HSP_BIN_DIR/hyperspace-supervisor" chmod +x "$HSP_BIN_DIR/hyperspace-supervisor" ok "Installed hyperspace-supervisor (run 'hyperspace supervisor install' to activate)" fi # Install agentix-reap shim (Linux only) — PR_SET_PDEATHSIG=SIGKILL exec-shim # that ties heavy inference engine children (shard-engine etc.) to the daemon at # the kernel level, so a daemon crash/kill -9 reaps the engine + its GPU VRAM # instead of orphaning it. Presence-gated on the Node side (wrapWithReapShim) so # its absence is a clean no-op. Kill switch: AGENTIX_REAP_SHIM=off. if [[ "$OS" == "linux" && -f "$TMP_DIR/agentix-reap" ]]; then cp "$TMP_DIR/agentix-reap" "$HSP_BIN_DIR/agentix-reap" chmod +x "$HSP_BIN_DIR/agentix-reap" ok "Installed agentix-reap (engine crash-tie shim)" fi # Install agentix-spawn-sandbox helper (Linux only) — the kernel confinement # helper agentix-confine's wrapSpawn prepends on Linux: a Landlock filesystem # write-allowlist (writes outside a sidecar's declared/granted dirs are kernel- # denied) + a seccomp-bpf syscall filter (blocks module-load/kexec/mount/…). # Presence-gated on the Node side (detectLinuxHelper) so its absence is a clean # policy-only passthrough — no regression. Kill switch: AGENTIX_CONFINE=off. if [[ "$OS" == "linux" && -f "$TMP_DIR/agentix-spawn-sandbox" ]]; then cp "$TMP_DIR/agentix-spawn-sandbox" "$HSP_BIN_DIR/agentix-spawn-sandbox" chmod +x "$HSP_BIN_DIR/agentix-spawn-sandbox" ok "Installed agentix-spawn-sandbox (Landlock/seccomp confinement helper)" fi # Install bash fallback (so the systemd timer can fall back if the Go binary fails for any reason) if [[ -f "$TMP_DIR/hyperspace-updater.sh" ]]; then cp "$TMP_DIR/hyperspace-updater.sh" "$HSP_BIN_DIR/hyperspace-updater.sh" chmod +x "$HSP_BIN_DIR/hyperspace-updater.sh" ok "Installed bash fallback updater (hyperspace-updater.sh)" if [[ "$OS" == "linux" ]] && command -v systemctl &>/dev/null; then SYSTEMD_USER_DIR="${HOME}/.config/systemd/user" mkdir -p "$SYSTEMD_USER_DIR" if [[ -f "$TMP_DIR/hyperspace-updater.service" ]]; then cp "$TMP_DIR/hyperspace-updater.service" "$SYSTEMD_USER_DIR/hyperspace-updater.service" fi if [[ -f "$TMP_DIR/hyperspace-updater.timer" ]]; then cp "$TMP_DIR/hyperspace-updater.timer" "$SYSTEMD_USER_DIR/hyperspace-updater.timer" fi systemctl --user daemon-reload 2>/dev/null || true systemctl --user enable --now hyperspace-updater.timer 2>/dev/null \ && ok "hyperspace-updater.timer enabled (every 5min, independent of main daemon)" \ || warn "Could not enable hyperspace-updater.timer — auto-update will rely on main daemon's checkForUpdate" elif [[ "$OS" == "darwin" ]]; then LAUNCHAGENTS_DIR="${HOME}/Library/LaunchAgents" mkdir -p "$LAUNCHAGENTS_DIR" if [[ -f "$TMP_DIR/com.hyperspace.updater.plist" ]]; then sed "s|HOME_PLACEHOLDER|${HOME}|g" "$TMP_DIR/com.hyperspace.updater.plist" \ > "$LAUNCHAGENTS_DIR/com.hyperspace.updater.plist" launchctl unload "$LAUNCHAGENTS_DIR/com.hyperspace.updater.plist" 2>/dev/null || true launchctl load "$LAUNCHAGENTS_DIR/com.hyperspace.updater.plist" 2>/dev/null \ && ok "com.hyperspace.updater LaunchAgent loaded (every 5min)" \ || warn "Could not load com.hyperspace.updater LaunchAgent" fi fi fi # Pod-raft coordinator — Go binary for Raft-backed pod state (BoltDB, no SQLite) # Installed to ~/.hyperspace/bin/pod-raft where the CLI auto-launches it on `hyperspace start` if [[ -f "$TMP_DIR/pod-raft" ]]; then cp "$TMP_DIR/pod-raft" "$BIN_DIR/pod-raft" chmod +x "$BIN_DIR/pod-raft" ok "Installed pod coordinator (pod-raft)" elif [[ -f "$TMP_DIR/pod-raft.exe" ]]; then cp "$TMP_DIR/pod-raft.exe" "$BIN_DIR/pod-raft.exe" ok "Installed pod coordinator (pod-raft.exe)" fi # Sidecars — 20 Tier-1 AgenticOS services. # Each sidecar is opt-in via its own HYPERSPACE_*_SIDECAR=1 env var. # Binaries live in ~/.hyperspace/bin and are auto-launched by the CLI. # Pre-existing: network inference pulse matrix # AgenticOS: vectors cache storage search context proxy wallet training # workflows thor-runtime thor-bridge catalog # plugin-runtime mcp-host skill-matrix # Blockchain: agentic-blockchain (A1 fullnode, auto-updates independently) for SIDECAR in \ network inference pulse matrix \ vectors cache storage search context proxy wallet training \ workflows thor-runtime thor-bridge catalog \ plugin-runtime mcp-host skill-matrix \ canvas assembly-graph loki automations \ agentic-blockchain; do BIN="hyperspace-$SIDECAR" if [[ -f "$TMP_DIR/$BIN" ]]; then cp "$TMP_DIR/$BIN" "$BIN_DIR/$BIN" chmod +x "$BIN_DIR/$BIN" ok "Installed $SIDECAR sidecar ($BIN)" elif [[ -f "$TMP_DIR/$BIN.exe" ]]; then cp "$TMP_DIR/$BIN.exe" "$BIN_DIR/$BIN.exe" ok "Installed $SIDECAR sidecar ($BIN.exe)" fi done # node-pty native addon — THE DAEMON'S OWN PTY DOORS depend on this. # It stopped being about loki (now a Go static binary on pure-Go creack/pty): # `apps/cli` externalizes node-pty from the SEA, and POST /api/v1/pty/open — # the door behind /term in /nb — loads it lazily at runtime. $BIN_DIR is # dirname(process.execPath) for the installed binary, which is exactly where # services/pty/pty-load.ts looks FIRST, so this is the co-location contract # `--external:node-pty` implies, not an optional extra. # MEASURED 2026-08-15: with it absent, /term answered # 501 {"ok":false,"error":"node-pty is not available on this host …"} # forever. Note this dir SURVIVES a bare binary swap, which is why a # scp-the-SEA deploy does not re-break /term once it is seated. if [[ -d "$TMP_DIR/node_modules/node-pty" ]]; then mkdir -p "$BIN_DIR/node_modules" rm -rf "$BIN_DIR/node_modules/node-pty" 2>/dev/null || true cp -R "$TMP_DIR/node_modules/node-pty" "$BIN_DIR/node_modules/node-pty" && \ ok "Installed node-pty native addon (the /term pty doors)" else warn "node-pty was not in this tarball — /term will answer 501 on this install" fi # Unprefixed Rust sidecars — invoked directly by name (no `hyperspace-` # prefix), so they ship + install under their bare crate-binary name. # grogu/our-pufferd/grogu-skill/swarm-distill — swarm-cognition substrate # spatialmesh-sidecar — TraceMesh graph projection # These ride in the tarball via the release-cli `SIDECAR_BINS` unprefixed # loop; without this seed they never reached ~/.local/bin (latent gap). for RBIN in grogu our-pufferd grogu-skill swarm-distill spatialmesh-sidecar; do if [[ -f "$TMP_DIR/$RBIN" ]]; then cp "$TMP_DIR/$RBIN" "$BIN_DIR/$RBIN" chmod +x "$BIN_DIR/$RBIN" ok "Installed $RBIN sidecar" fi done # ─── avmd sidecar (security gate) ──────────────────────────────────────────── # avmd is the system-wide AVM gate. Bundled in the CLI tarball when the # release-cli workflow could fetch it from releases.hyper.space. Lands at # ~/.hyperspace/bin/avmd; the CLI's AvmBinaryManager picks it up at # `hyperspace start`. If not bundled (e.g. Intel Mac, where the avmd # release matrix has no asset), the CLI falls back to its own download # path on first start, or politely declines to enable AVM gating. AVMD_BIN_DIR="${HOME}/.hyperspace/bin" mkdir -p "$AVMD_BIN_DIR" if [[ -f "$TMP_DIR/avmd" ]]; then cp "$TMP_DIR/avmd" "$AVMD_BIN_DIR/avmd" chmod +x "$AVMD_BIN_DIR/avmd" ok "Installed avmd sidecar" fi if [[ -f "$TMP_DIR/avmctl" ]]; then cp "$TMP_DIR/avmctl" "$AVMD_BIN_DIR/avmctl" chmod +x "$AVMD_BIN_DIR/avmctl" ok "Installed avmctl operator CLI" fi if [[ -f "$TMP_DIR/.avmd-version" ]]; then cp "$TMP_DIR/.avmd-version" "$AVMD_BIN_DIR/.avmd-version" fi # ─── Python shard scripts (distributed inference, legacy path) ────────────── # mlx_shard_server.py (Apple Silicon) and torch_shard_server.py (NVIDIA CUDA) # are the v5.75-line shard servers, still shipped for backward compat. # v5.80+: the Rust `shard-engine` binary below supersedes these and avoids # the `pip install torch transformers` pre-flight. # # v5.80.2 hardening: prior install.sh used silent `cp ... 2>/dev/null` for # these files. If the destination was owned by another UID, was read-only, # or any other surprise condition existed, the cp failed silently — leaving # a stale .py from a prior install in place. The next spawn would crash # referencing line numbers from the old file (e.g. "vocab_size=1" tokenizer # load followed by an unrelated traceback) and surface as "did not become # ready within 900s" with no useful diagnostic. Always force-overwrite, # always print version, fail loudly when a copy fails. PYTHON_DIR="${HOME}/.hyperspace/python" mkdir -p "$PYTHON_DIR" if [[ -d "$TMP_DIR/python" ]]; then shard_script_failures=0 for src in "$TMP_DIR/python/"*.py; do [[ -f "$src" ]] || continue dst="$PYTHON_DIR/$(basename "$src")" # Remove first so a permission/lock mismatch surfaces as an explicit # cp error rather than a silent no-op overwrite. rm -f "$dst" 2>/dev/null || true if ! install -m 644 "$src" "$dst" 2>/dev/null && ! cp -f "$src" "$dst" 2>/dev/null; then warn "Could not install $(basename "$src") to $PYTHON_DIR — check permissions" shard_script_failures=$((shard_script_failures + 1)) fi done if [[ $shard_script_failures -eq 0 ]]; then # Report what's installed so users can verify it's not a stale copy. fingerprint="" for f in "$PYTHON_DIR/mlx_shard_server.py" "$PYTHON_DIR/torch_shard_server.py"; do if [[ -f "$f" ]]; then # First line of the file's "tied_embeddings" annotation as a version # marker. v5.75+ has it; older versions don't. if grep -q "tied_embeddings" "$f" 2>/dev/null; then fingerprint+="$(basename "$f")=v5.75+ " else fingerprint+="$(basename "$f")=pre-v5.75 " fi fi done ok "Installed shard inference scripts (legacy Python path) — $fingerprint" else warn "$shard_script_failures shard script(s) failed to update — distributed inference may use stale code" fi fi # ─── shard-engine Rust binary (v5.80+ canonical path) ─────────────────────── # Single binary, candle backend (CUDA / Metal / CPU). Drop-in replacement for # the Python shard servers. When present in ~/.hyperspace/bin/, the CLI's # TorchShardClient + MLXShardClient detect it and spawn it instead of # python3 + the legacy script. No `pip install` step required. if [[ -f "$TMP_DIR/shard-engine" || -f "$TMP_DIR/shard-engine.exe" ]]; then for name in shard-engine shard-engine.exe; do if [[ -f "$TMP_DIR/$name" ]]; then cp "$TMP_DIR/$name" "$BIN_DIR/$name" && \ chmod +x "$BIN_DIR/$name" && \ ok "Installed shard-engine ($(${BIN_DIR}/$name --version 2>/dev/null | head -1 || echo unknown))" || true fi done fi # ─── Companion sidecar SEAs (v5.82.15+) ──────────────────────────────────── # Pure-Node SEAs for the paid-tool router (@hyperspace/tool-exchange) and # the Apify-compatible run engine (@hyperspace/actor-engine). Both default # OFF — the OS service-supervisor only spawns them when TOOL_EXCHANGE_ENABLED # / ACTOR_ENGINE_ENABLED is set + credentials provided. Shipping the binary # is therefore inert until ops turns it on. for name in hyperspace-tool-exchange hyperspace-actor-engine; do if [[ -f "$TMP_DIR/$name" ]]; then cp "$TMP_DIR/$name" "$BIN_DIR/$name" && \ chmod +x "$BIN_DIR/$name" && \ ok "Installed companion sidecar: $name (default OFF)" || true fi done # ─── Standalone-package sidecar SEAs (loki, canvas, assembly-graph) ───────── # hyperspace-loki is now a GO static binary (backs loki.toml's launcher=go-static); # hyperspace-canvas / hyperspace-assembly-graph / hyperspace-tracemesh are Node # SEAs; hyperspace-automations is the user-automations trigger engine. The # service-supervisor spawns these by bare name (e.g. spawn('hyperspace-loki') # → `serve --port 8095`), so they MUST land on $BIN_DIR (PATH). Each guard tolerates a # missing binary so an older tarball that predates a given sidecar installs cleanly. for name in hyperspace-loki hyperspace-canvas hyperspace-assembly-graph hyperspace-tracemesh hyperspace-automations; do if [[ -f "$TMP_DIR/$name" ]]; then cp "$TMP_DIR/$name" "$BIN_DIR/$name" && \ chmod +x "$BIN_DIR/$name" && \ ok "Installed sidecar: $name" || true fi done # ─── Migrated (cjs-shim) sidecars (INC-3) ─────────────────────────────────── # A migrated sidecar does NOT ship a fat ~117MB self-contained SEA. Instead the # tarball carries a plain `hyperspace-.cjs` bundle which we install into # ~/.hyperspace/bundles/ and run via a SHARED node-runtime through a tiny # POSIX-sh launcher at ~/.hyperspace/bin/hyperspace-: # # exec "/node-runtime" "/hyperspace-.cjs" "$@" # # The supervisor/warmer resolve a sidecar by name with ~/.hyperspace/bin # checked FIRST (see resolveBinaryCandidates in local-sidecars.ts), so the shim # lands there and shadows any older SEA in ~/.local/bin. We mirror the existing # sidecar loops: each entry is tolerant — a tarball that predates a given # migrated sidecar (no hyperspace-.cjs present) just skips it. This block # is byte-equivalent to the per-sidecar shim the auto-update bundle path writes # in apps/cli/src/commands/start.ts (same >1024-byte padded shape so the # warmer's findBinary size>1024 presence check passes). HSP_BUNDLE_DIR="${HOME}/.hyperspace/bundles" mkdir -p "$HSP_BUNDLE_DIR" "$HSP_BIN_DIR" # Seed the shared node-runtime if the tarball carries one (the cjs-shim execs # it). Already present from a prior bundle install? Re-seed anyway so a fresh # SEA-tarball install of a migrated sidecar can run. Tolerant when absent. if [[ -f "$TMP_DIR/node-runtime" ]]; then cp "$TMP_DIR/node-runtime" "$HSP_BIN_DIR/node-runtime" && \ chmod +x "$HSP_BIN_DIR/node-runtime" && \ ok "Installed shared node-runtime (cjs-shim sidecars)" || true fi # ORT-Web WASM next to the SHARED node-runtime — the migrated search sidecar # (hyperspace-search.cjs) runs under node-runtime in HSP_BIN_DIR, and ORT-Web # loads these blobs relative to process.execPath's dir (= HSP_BIN_DIR). The # earlier copy (BIN_DIR/~/.local/bin) covers the legacy fat-SEA search binary; # the .cjs-under-node-runtime needs them HERE. Tolerant when absent. ORT_HSP_INSTALLED=0 for f in ort-wasm-simd-threaded.jsep.wasm ort-wasm-simd-threaded.jsep.mjs ort-wasm-simd-threaded.wasm ort-wasm-simd-threaded.mjs; do if [[ -f "$TMP_DIR/$f" ]]; then cp "$TMP_DIR/$f" "$HSP_BIN_DIR/$f" && ORT_HSP_INSTALLED=$((ORT_HSP_INSTALLED + 1)) fi done [[ $ORT_HSP_INSTALLED -gt 0 ]] && ok "Installed ORT-Web WASM next to node-runtime ($ORT_HSP_INSTALLED files — search .cjs)" # write_cjs_shim — emit ~/.hyperspace/bin/hyperspace- atomically # (tmp + mv, 0755). The body is a valid sh script whose ONLY executable # statement is the trailing exec; the comment banner pads it >1024 bytes so the # warmer's findBinary presence check (size>1024 == present) passes. write_cjs_shim() { shim_id="$1" shim_target="$HSP_BIN_DIR/hyperspace-${shim_id}" shim_runtime="$HSP_BIN_DIR/node-runtime" shim_cjs="$HSP_BUNDLE_DIR/hyperspace-${shim_id}.cjs" shim_tmp="${shim_target}.cjs-shim.tmp.$$" { printf '%s\n' '#!/bin/sh' printf '# hyperspace cjs-shim launcher for sidecar id: %s\n' "$shim_id" printf '%s\n' '#' printf '%s\n' '# Generated by install.sh (INC-3). Execs a SHARED node runtime against' printf '%s\n' '# this sidecar'\''s plain .cjs bundle so the sidecar does NOT ship its own' printf '%s\n' '# SEA. DO NOT hand-edit.' printf '# runtime : %s\n' "$shim_runtime" printf '# bundle : %s\n' "$shim_cjs" printf '%s\n' '#' printf '%s\n' '# ── Size-padding banner (load-bearing) ──────────────────────────────' printf '%s\n' '# The warmer findBinary() presence check treats a binary whose on-disk' printf '%s\n' '# size is <= 1024 bytes as ABSENT. A naive ~60-byte shim would be' printf '%s\n' '# reported "missing" every boot and never warm, so we pad with the' printf '%s\n' '# comment block below: sh ignores every "#" line, leaving the single' printf '%s\n' '# trailing exec as the ONLY executable statement. Padding is inert.' printf '%s\n' '#' i=0 while [[ $i -lt 18 ]]; do printf '# pad %02d :: hyperspace cjs-shim launcher presence-padding ································································\n' "$i" i=$((i + 1)) done printf '%s\n' '# ────────────────────────────────────────────────────────────────────' printf 'exec "%s" "%s" "$@"\n' "$shim_runtime" "$shim_cjs" } > "$shim_tmp" chmod 755 "$shim_tmp" mv -f "$shim_tmp" "$shim_target" chmod 755 "$shim_target" } # Migrated set — MANIFEST-DRIVEN: install EVERY hyperspace-.cjs the tarball # carries (build-release-tarballs.sh stages each migrated .cjs into the tarball # root alongside its shared node-runtime). This auto-covers the full Tier-A set # (proxy/cache/inference/network/…) without a hand-maintained list that drifts. # Each: seed the .cjs into ~/.hyperspace/bundles + (re)write a portable $HOME # cjs-shim into ~/.hyperspace/bin so the supervisor/warmer can spawn it. Absent # → loop body skipped (nullglob-safe via the -e guard). shopt -s nullglob 2>/dev/null || true for CJS_PATH in "$TMP_DIR"/hyperspace-*.cjs; do [[ -e "$CJS_PATH" ]] || continue CJS="$(basename "$CJS_PATH")" MIGRATED="${CJS#hyperspace-}"; MIGRATED="${MIGRATED%.cjs}" cp "$CJS_PATH" "$HSP_BUNDLE_DIR/$CJS" && \ write_cjs_shim "$MIGRATED" && \ ok "Installed migrated sidecar (cjs-shim): hyperspace-$MIGRATED" || \ warn "Could not install migrated sidecar hyperspace-$MIGRATED" done # tracemesh-core native addon for the tracemesh sidecar — externalized from its # SEA (the napi-rs engine can't live in a SEA blob) and loaded at runtime by the # engine's resolver, which searches next to the binary ($BIN_DIR) + ~/.hyperspace/bin. # MANDATORY (unlike loki's node-pty): without it the sidecar can't serve, so we # seed the platform-matching hyperspace-tracemesh-core.-.node here. for f in "$TMP_DIR"/hyperspace-tracemesh-core.*.node; do if [[ -f "$f" ]]; then cp "$f" "$HSP_BIN_DIR/$(basename "$f")" && \ ok "Installed tracemesh engine: $(basename "$f")" || true fi done # ─── Skills (Pi coding agent) ───────────────────────────────────────────── SKILLS_DIR="${HOME}/.hyperspace/skills" mkdir -p "$SKILLS_DIR" if [[ -d "$TMP_DIR/skills" ]]; then cp "$TMP_DIR/skills/"*.md "$SKILLS_DIR/" 2>/dev/null && \ ok "Installed Hyperspace skill for Pi coding agent" || true fi # ─── A1 Blockchain Node (NOT installed by the public installer) ────────────── # Scoping law (2026-09-15): the swarm install is node + serve + earn. The A1 # chain binary lives in the PRIVATE agi repo — an anonymous download can never # succeed, so shipping the attempt gave every public install a scary warning # (and, before the set-e fix, a fatal abort). Chain participants install it # explicitly via `hyperspace chain install`; the daemon lazy-fetches it when # chain features are first used. # ─── Install Prometheus Cognitive Engine ───────────────────────────────────── # Downloaded ONLY if not already present. Prometheus auto-updates itself # independently — install.sh NEVER overwrites an existing bundle. RUNTIME_DIR="${HOME}/.hyperspace/runtime" mkdir -p "$RUNTIME_DIR" if [[ ! -f "$RUNTIME_DIR/prometheus.bundle.js" ]]; then step "Downloading Prometheus cognitive engine..." PROM_MANIFEST=$(curl -sfL "https://agents.hyper.space/api/prometheus/latest" 2>/dev/null || echo "") if [[ -n "$PROM_MANIFEST" ]]; then PROM_URL=$(echo "$PROM_MANIFEST" | grep -o '"url":"[^"]*"' | head -1 | cut -d'"' -f4) PROM_CHECKSUM=$(echo "$PROM_MANIFEST" | grep -o '"checksum":"[^"]*"' | head -1 | cut -d'"' -f4) PROM_VERSION=$(echo "$PROM_MANIFEST" | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4) if [[ -n "$PROM_URL" ]]; then curl -sfL "$PROM_URL" -o "$RUNTIME_DIR/prometheus.bundle.js.tmp" 2>/dev/null if [[ -f "$RUNTIME_DIR/prometheus.bundle.js.tmp" ]]; then # Verify checksum if provided ACTUAL_SUM=$(sha256sum "$RUNTIME_DIR/prometheus.bundle.js.tmp" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$RUNTIME_DIR/prometheus.bundle.js.tmp" 2>/dev/null | cut -d' ' -f1) if [[ -z "$PROM_CHECKSUM" || "$ACTUAL_SUM" == "$PROM_CHECKSUM" ]]; then mv "$RUNTIME_DIR/prometheus.bundle.js.tmp" "$RUNTIME_DIR/prometheus.bundle.js" echo "{\"version\":\"$PROM_VERSION\",\"checksum\":\"$ACTUAL_SUM\",\"updatedAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$RUNTIME_DIR/version.json" ok "Prometheus $PROM_VERSION installed" else rm -f "$RUNTIME_DIR/prometheus.bundle.js.tmp" warn "Prometheus checksum mismatch — will download on first 'hyperspace start'" fi else warn "Prometheus download failed — will retry on first 'hyperspace start'" fi fi else warn "Could not reach Prometheus manifest — will download on first 'hyperspace start'" fi else ok "Prometheus already installed — skipping (auto-updates independently)" fi # Install to /usr/local/bin/ — critical to avoid stale-binary PATH conflicts. # Previous installs may have placed an older version there, and /usr/local/bin # typically comes before ~/.local/bin in PATH. If we skip this, `hyperspace` # resolves to the OLD binary while ~/.local/bin/hyperspace has the new one. if [[ -d "/usr/local/bin" ]]; then if cp "$BINARY" /usr/local/bin/hyperspace 2>/dev/null && chmod +x /usr/local/bin/hyperspace 2>/dev/null; then ok "Installed to /usr/local/bin/hyperspace" cp "$BINARY" /usr/local/bin/aios-cli 2>/dev/null && chmod +x /usr/local/bin/aios-cli 2>/dev/null || true elif [[ "$(id -u)" == "0" ]]; then # We're root but cp failed (shouldn't happen) — try explicit paths install -m 755 "$BINARY" /usr/local/bin/hyperspace install -m 755 "$BINARY" /usr/local/bin/aios-cli 2>/dev/null || true ok "Installed to /usr/local/bin/hyperspace (root)" elif [[ "$USE_SUDO" == "true" ]]; then sudo cp "$BINARY" /usr/local/bin/hyperspace && sudo chmod +x /usr/local/bin/hyperspace ok "Installed to /usr/local/bin/hyperspace (sudo)" sudo cp "$BINARY" /usr/local/bin/aios-cli 2>/dev/null && sudo chmod +x /usr/local/bin/aios-cli 2>/dev/null || true else # Check if there's an OLD binary at /usr/local/bin that will shadow us if [[ -f "/usr/local/bin/hyperspace" ]]; then OLD_VER=$(/usr/local/bin/hyperspace version 2>/dev/null | head -1 || echo 'unknown') NEW_VER=$("$BIN_DIR/hyperspace" version 2>/dev/null | head -1 || echo 'unknown') if [[ "$OLD_VER" != "$NEW_VER" ]]; then warn "Stale binary at /usr/local/bin/hyperspace ($OLD_VER) shadows new install ($NEW_VER)" warn "Fix: sudo cp $BIN_DIR/hyperspace /usr/local/bin/hyperspace" fi fi fi fi # Also install to ~/.hyperspace for backward compat cp "$BIN_DIR/aios-cli" "$INSTALL_DIR/aios-cli" # Write install-method marker (CLI reports this in analytics sync) echo "install-script" > "$INSTALL_DIR/install-method" # v1 compat: install to common v1 locations if they exist if [[ -d "${HOME}/.aios" ]]; then cp "$BIN_DIR/aios-cli" "${HOME}/.aios/aios-cli" 2>/dev/null || true ok "Updated v1 binary at ~/.aios/aios-cli" fi # ─── Install llama-server (inference engine) ─── step "Installing llama-server (AI inference engine)..." LLAMA_BUILD="b8148" LLAMA_BIN_DIR="${HOME}/.hyperspace/bin" LLAMA_SERVER_PATH="${LLAMA_BIN_DIR}/llama-server" LLAMA_VERSION_FILE="${LLAMA_BIN_DIR}/.llama-server-version" # Check if already installed at the right version if [[ -f "$LLAMA_SERVER_PATH" ]] && [[ -f "$LLAMA_VERSION_FILE" ]] && [[ "$(cat "$LLAMA_VERSION_FILE")" == "$LLAMA_BUILD" ]]; then ok "llama-server $LLAMA_BUILD already installed" else # Determine archive name HAS_NVIDIA=false command -v nvidia-smi &>/dev/null && HAS_NVIDIA=true case "$OS-$ARCH" in linux-x86_64) LLAMA_ARCHIVE="llama-${LLAMA_BUILD}-bin-ubuntu-x64.tar.gz" ;; linux-aarch64|linux-arm64) # Upstream llama.cpp publishes no linux-arm64 binary; the daemon's # llama-prism lane installs its own build on first model load. LLAMA_ARCHIVE="" ;; darwin-arm64) LLAMA_ARCHIVE="llama-${LLAMA_BUILD}-bin-macos-arm64.tar.gz" ;; darwin-x86_64) LLAMA_ARCHIVE="llama-${LLAMA_BUILD}-bin-macos-x64.tar.gz" ;; esac if [[ -z "${LLAMA_ARCHIVE:-}" ]]; then warn "No upstream llama-server binary for $OS-$ARCH — skipping (llama-prism installs on first model load)" else LLAMA_URL="https://github.com/ggml-org/llama.cpp/releases/download/${LLAMA_BUILD}/${LLAMA_ARCHIVE}" info "Downloading llama-server from $LLAMA_URL" mkdir -p "$LLAMA_BIN_DIR" if curl -fSL --progress-bar -o "$TMP_DIR/$LLAMA_ARCHIVE" "$LLAMA_URL"; then # Extract just the llama-server binary tar xzf "$TMP_DIR/$LLAMA_ARCHIVE" -C "$TMP_DIR" "llama-${LLAMA_BUILD}/llama-server" 2>/dev/null || \ tar xzf "$TMP_DIR/$LLAMA_ARCHIVE" -C "$TMP_DIR" if [[ -f "$TMP_DIR/llama-${LLAMA_BUILD}/llama-server" ]]; then mv "$TMP_DIR/llama-${LLAMA_BUILD}/llama-server" "$LLAMA_SERVER_PATH" elif [[ -f "$TMP_DIR/llama-server" ]]; then mv "$TMP_DIR/llama-server" "$LLAMA_SERVER_PATH" fi if [[ -f "$LLAMA_SERVER_PATH" ]]; then chmod +x "$LLAMA_SERVER_PATH" echo "$LLAMA_BUILD" > "$LLAMA_VERSION_FILE" ok "Installed llama-server to $LLAMA_SERVER_PATH" else warn "llama-server binary not found in archive — will download on first run" fi else warn "Failed to download llama-server — will download on first run" fi fi fi # ─── Install uv (Python package manager for autoresearch) ─── step "Installing uv (Python package manager)..." UV_PATH="${HOME}/.local/bin/uv" if command -v uv &>/dev/null; then UV_VER=$(uv --version 2>/dev/null | head -1) ok "uv already installed: $UV_VER" elif [[ -f "$UV_PATH" ]]; then UV_VER=$("$UV_PATH" --version 2>/dev/null | head -1) ok "uv already installed: $UV_VER" else # Install uv via the official installer (single binary, ~30MB) if curl -fsSL https://astral.sh/uv/install.sh | sh 2>/dev/null; then UV_VER=$("$UV_PATH" --version 2>/dev/null || uv --version 2>/dev/null || echo "installed") ok "uv installed: $UV_VER" else warn "uv install failed — GPU training will use TypeScript fallback" fi fi # ─── Kill existing processes ─── # Stop any running node or tray so the new install starts clean pkill -f "hyperspace start" 2>/dev/null || true pkill -f "hyperspace-tray" 2>/dev/null || true pkill -f "Hyperspace.app" 2>/dev/null || true sleep 1 # ─── Install tray app on desktop (first install only) ─── # The tray app requires manual auth (sudo, Gatekeeper) which blocks auto-updates. # Only install on first run; after that, the tray app auto-updates itself. # Skip entirely on headless servers or if already installed. TRAY_ALREADY_INSTALLED=false [[ -d "/Applications/Hyperspace.app" ]] && TRAY_ALREADY_INSTALLED=true command -v hyperspace-tray &>/dev/null && TRAY_ALREADY_INSTALLED=true if [[ "$IS_DESKTOP" == "true" ]] && [[ "$NO_TRAY" == "false" ]] && [[ "$TRAY_ALREADY_INSTALLED" == "false" ]]; then step "Installing Hyperspace Tray App (first time only)..." # Determine tray asset name for this platform TRAY_ASSET="" TRAY_INSTALL_METHOD="" case "$OS-$ARCH" in darwin-arm64) TRAY_ASSET="Hyperspace_.*_aarch64.dmg" TRAY_INSTALL_METHOD="dmg" ;; darwin-x86_64) TRAY_ASSET="Hyperspace_.*_x64.dmg" TRAY_INSTALL_METHOD="dmg" ;; linux-x86_64) TRAY_ASSET="Hyperspace_.*_amd64.deb" TRAY_INSTALL_METHOD="deb" ;; esac if [[ -n "$TRAY_ASSET" ]]; then # Get tray app from hyperspace-node releases (public repo, tray-v* tags) TRAY_RELEASE_API="https://api.github.com/repos/hyperspaceai/hyperspace-node/releases?per_page=10" TRAY_URL=$(curl -fsSL --connect-timeout 10 "$TRAY_RELEASE_API" 2>/dev/null | grep -oE "\"browser_download_url\": *\"[^\"]*${TRAY_ASSET}\"" | head -1 | cut -d'"' -f4 || true) # Tray install is best-effort — never block the CLI from starting if [[ -n "$TRAY_URL" ]]; then ( set +e # disable errexit in subshell so failures don't kill the script TRAY_FILE=$(basename "$TRAY_URL") info "Downloading tray app: $TRAY_FILE" curl -fSL --connect-timeout 15 --progress-bar -o "$TMP_DIR/$TRAY_FILE" "$TRAY_URL" || { warn "Tray download failed — skipping"; exit 0; } case "$TRAY_INSTALL_METHOD" in dmg) MOUNT_DIR=$(hdiutil attach "$TMP_DIR/$TRAY_FILE" -nobrowse -noverify -noautoopen 2>/dev/null | tail -1 | awk '{print $3}') if [[ -n "$MOUNT_DIR" ]] && [[ -d "$MOUNT_DIR" ]]; then APP_PATH=$(find "$MOUNT_DIR" -maxdepth 1 -name "*.app" -print -quit 2>/dev/null) if [[ -n "$APP_PATH" ]]; then INSTALLED_APP="/Applications/$(basename "$APP_PATH")" rm -rf "$INSTALLED_APP" 2>/dev/null || sudo rm -rf "$INSTALLED_APP" 2>/dev/null || true cp -R "$APP_PATH" /Applications/ 2>/dev/null || sudo cp -R "$APP_PATH" /Applications/ 2>/dev/null || true xattr -cr "$INSTALLED_APP" 2>/dev/null || true chmod +x "$INSTALLED_APP/Contents/MacOS/"* 2>/dev/null || true spctl --add "$INSTALLED_APP" 2>/dev/null || true ok "Tray app installed to $INSTALLED_APP" # Launch the tray app open "$INSTALLED_APP" 2>/dev/null || true fi hdiutil detach "$MOUNT_DIR" -quiet 2>/dev/null || true else warn "Could not mount DMG — tray app not installed" fi ;; deb) if command -v dpkg &>/dev/null; then sudo dpkg -i "$TMP_DIR/$TRAY_FILE" 2>/dev/null || sudo apt-get install -f -y 2>/dev/null || true ok "Tray app installed via dpkg" # Launch the tray app nohup hyperspace-tray >/dev/null 2>&1 & disown 2>/dev/null || true elif command -v apt &>/dev/null; then sudo apt install -y "$TMP_DIR/$TRAY_FILE" 2>/dev/null || true ok "Tray app installed via apt" nohup hyperspace-tray >/dev/null 2>&1 & disown 2>/dev/null || true else warn "No compatible installer found — tray app not installed" fi ;; esac ) || true # ensure subshell failure never kills the parent else warn "Tray app not found in latest release — skipping" fi fi fi # ─── Set private key if provided ─── if [[ -n "$PRIVATE_KEY" ]]; then export HYPERSPACE_PRIVATE_KEY="$PRIVATE_KEY" info "Using provided private key" fi # ─── System info ─── step "Detecting hardware..." "$BIN_DIR/hyperspace" system-info # ─── Identity ─── step "Setting up identity..." "$BIN_DIR/hyperspace" hive whoami 2>/dev/null || true # ─── Claude Code onboarding (the one-command experience) ───────────────────── # If Claude Code lives on this machine, finish the whole integration here: # MCP server registration (user scope ~/.claude.json via `claude mcp add`), # the eva/thor subagents, and the CLAUDE.md tips — exactly what # `hyperspace setup claude-code` does, run for the user so install→onboard # is ONE command. Detection: the `claude` CLI on PATH, or an existing # ~/.claude directory (desktop/IDE installs without the CLI on PATH — the # setup's direct ~/.claude.json merge covers those). Soft: any failure # warns and never blocks the install. Opt out: HYPERSPACE_NO_CLAUDE_SETUP=1. if [[ "${HYPERSPACE_NO_CLAUDE_SETUP:-0}" != "1" ]]; then if command -v claude >/dev/null 2>&1 || [[ -d "$HOME/.claude" ]]; then step "Claude Code detected — installing MCP server + eva/thor subagents..." # The CLAUDE.md guidance rides the unattended path too, because it is now # a MANAGED FENCED SECTION (marker-delimited: created once, updated in # place, user prose outside the fence never touched, and deleting the # markers hands the section to the user permanently) — the same contract # the subagent files use. Full experience in one command. if "$BIN_DIR/hyperspace" setup claude-code 2>/dev/null; then ok "Claude Code wired — restart Claude Code and try: \"check my hyperspace status\"" else warn "Claude Code setup incomplete — run 'hyperspace setup claude-code' manually" fi fi # Codex parity: the SAME MCP server + AGENTS.md section — whichever # assistant the user installs from, hyperspace operates both. if command -v codex >/dev/null 2>&1 || [[ -d "$HOME/.codex" ]]; then step "Codex detected — installing MCP server + AGENTS.md section..." if "$BIN_DIR/hyperspace" setup codex 2>/dev/null; then ok "Codex wired — restart codex and try: \"check my hyperspace node status\"" else warn "Codex setup incomplete — run 'hyperspace setup codex' manually" fi fi fi # ─── Register daemon for OS-level auto-start (v5.45.36+) ───────────────────── # Without this, a daemon that stops for any reason (laptop closed, sleep, # crash, manual kill) stays stopped — and a stopped daemon doesn't poll for # updates, so the node strands on whatever version was last running. # Registering at install time means: ANY future reboot brings the daemon # back, which immediately polls + updates if anything's stale. # macOS: ~/Library/LaunchAgents/space.hyper.agent.plist (RunAtLoad+KeepAlive) # Linux: ~/.config/systemd/user/hyperspace.service + loginctl enable-linger # Windows: HKCU\...\Run\Hyperspace # Opt out via HYPERSPACE_NO_AUTOSTART=1. if [[ "${HYPERSPACE_NO_AUTOSTART:-0}" != "1" ]]; then step "Registering daemon for auto-start at boot..." "$BIN_DIR/hyperspace" install-service 2>/dev/null \ && ok "Daemon registered for OS-level auto-start" \ || warn "Could not register auto-start (run 'hyperspace install-service' manually if desired)" fi # ─── Done ─── echo "" echo -e "${GREEN}${BOLD}Installation complete!${NC}" echo "" INSTALLED_VER=$("$BIN_DIR/hyperspace" version 2>/dev/null | head -1 || echo 'unknown') echo " Version: $INSTALLED_VER" echo "" # Verify the PATH-resolved binary matches what we just installed PATH_BIN=$(command -v hyperspace 2>/dev/null || true) if [[ -n "$PATH_BIN" ]] && [[ "$PATH_BIN" != "$BIN_DIR/hyperspace" ]]; then PATH_VER=$("$PATH_BIN" version 2>/dev/null | head -1 || echo 'unknown') if [[ "$PATH_VER" != "$INSTALLED_VER" ]]; then echo "" warn "PATH conflict detected!" warn " Installed: $BIN_DIR/hyperspace ($INSTALLED_VER)" warn " PATH resolves to: $PATH_BIN ($PATH_VER)" warn " Fix: run 'sudo cp $BIN_DIR/hyperspace $PATH_BIN'" # Try to fix it automatically if cp "$BIN_DIR/hyperspace" "$PATH_BIN" 2>/dev/null && chmod +x "$PATH_BIN" 2>/dev/null; then ok "Fixed: updated $PATH_BIN to $INSTALLED_VER" elif [[ "$USE_SUDO" == "true" ]]; then sudo cp "$BIN_DIR/hyperspace" "$PATH_BIN" && sudo chmod +x "$PATH_BIN" ok "Fixed: updated $PATH_BIN to $INSTALLED_VER (sudo)" else warn "Run with --sudo to fix, or manually: sudo cp $BIN_DIR/hyperspace $PATH_BIN" fi fi fi if [[ "$IS_DESKTOP" == "true" ]] && [[ "$NO_TRAY" == "false" ]]; then echo -e " ${CYAN}Tray app installed — runs in system tray with auto-update${NC}" echo "" fi if [[ "$V1_MIGRATED" == "true" ]]; then echo -e "${CYAN}Upgraded from v1 to v2!${NC}" echo " Your v1 identity will be auto-migrated on first run." echo " Points earned in v1 are preserved in your account." echo "" fi echo -e "${BOLD}Your node capabilities:${NC}" echo "" echo -e " ${GREEN}Research${NC} Autonomous experiments — training scripts evolve via mutations + LLM" echo -e " ${GREEN}Inference${NC} Serve AI models to the network (3-tier: local/DHT/gossip)" echo -e " ${GREEN}Embedding${NC} CPU vector embeddings (all-MiniLM-L6-v2)" echo -e " ${GREEN}Storage${NC} Content-addressed blocks on distributed DHT" echo -e " ${GREEN}Memory${NC} Distributed vector store with replication" echo -e " ${GREEN}Relay${NC} Circuit relay v2 for NAT traversal" echo -e " ${GREEN}Validation${NC} Pulse rounds with structural verification" echo -e " ${GREEN}Proxy${NC} Residential IP proxy for agents" echo -e " ${GREEN}Agent Brain${NC} Autonomous cognitive loop — goals, memory, journal, strategy" echo "" echo -e " ${BOLD}23 models${NC} available (auto-download best for your GPU)" echo -e " ${BOLD}Points${NC} earned for uptime + capabilities + work (utility mining)" echo "" echo -e " Full changelog: ${CYAN}https://agents.hyper.space/features${NC}" echo "" echo -e "${BOLD}Commands:${NC}" echo "" echo -e " ${CYAN}hyperspace start${NC} Start node" echo -e " ${CYAN}hyperspace hive whoami${NC} Identity + points" echo -e " ${CYAN}hyperspace models pull --auto${NC} Download best models for your GPU" echo -e " ${CYAN}hyperspace system-info${NC} Hardware info" echo -e " ${CYAN}hyperspace update${NC} Check for updates" echo -e " ${CYAN}hyperspace install-service${NC} Auto-restart on crash/reboot" echo "" echo -e " Dashboard: ${CYAN}https://agents.hyper.space${NC}" echo "" # ─── One-click LAN join ─── # Redeem a trusted LAN-pod token BEFORE starting. For a LAN token the redeem # writes a trusted pod.json (trustedTransport:true); `hyperspace start` then # advertises the pinned pod_id over mDNS and converges with the coordinator on # the LAN — no server, no relay. We strip a /join/ URL wrapper if given. # pod_member_count — parse the online member count from `hyperspace pod status # --json`. The JSON shape (see apps/cli/src/commands/pod.ts emitJson) is: # { "pod": {...}, "cluster": { "onlineNodes": N, "nodes": [...] }, ... } # Prefer jq when present; otherwise fall back to a grep/sed parse of the # `onlineNodes` field. Echoes an integer (0 when unknown/unreachable). Pure # parse helper — best-effort, never fails the install. pod_member_count() { pmc_json="$1" [[ -z "$pmc_json" ]] && { echo 0; return; } if command -v jq >/dev/null 2>&1; then # `// 0` guards a missing field; `// empty` guards malformed JSON (jq errors # to stderr and prints nothing → the `|| echo 0` below covers it). pmc_n=$(printf '%s' "$pmc_json" | jq -r '.cluster.onlineNodes // 0' 2>/dev/null || echo 0) else # jq-absent fallback: grab the first `"onlineNodes": N` integer. pmc_n=$(printf '%s' "$pmc_json" \ | grep -o '"onlineNodes"[[:space:]]*:[[:space:]]*[0-9]\{1,\}' \ | head -1 | grep -o '[0-9]\{1,\}$' || echo 0) fi # Coerce non-numeric / empty to 0. case "$pmc_n" in ''|*[!0-9]*) echo 0 ;; *) echo "$pmc_n" ;; esac } # converge_check — after a join, poll `pod status --json` for up to ~15s waiting # for the coordinator to report >=2 online members. Honest: a written pod.json # is NOT a joined ring, so we print "converged" only on real membership, and a # patient "waiting for the coordinator" otherwise — never a false success. # Best-effort: any failure (CLI error, coordinator unreachable, timeout) just # falls through to the waiting message; it never aborts the install. converge_check() { cc_deadline=$(( $(date +%s) + 15 )) cc_count=0 while [[ $(date +%s) -lt $cc_deadline ]]; do cc_json=$("$BIN_DIR/hyperspace" pod status --json 2>/dev/null || true) cc_count=$(pod_member_count "$cc_json") if [[ "$cc_count" -ge 2 ]]; then ok "✓ converged with $cc_count device(s)" return 0 fi sleep 2 done warn "⏳ joined — waiting for the coordinator (run \`hyperspace lan\` on the host machine)" return 0 } if [[ -n "$JOIN_TOKEN" ]]; then JOIN_TOKEN="${JOIN_TOKEN##*/join/}" # tolerate a full https://.../join/ URL step "Joining your trusted LAN pod..." if "$BIN_DIR/hyperspace" pod join "$JOIN_TOKEN"; then ok "Joined the LAN pod." # Don't claim "hyperspaced" off a written pod.json — verify real convergence # with the coordinator (best-effort; never fails the install on a timeout). converge_check || true else warn "Auto-join did not complete. Run it manually:" warn " hyperspace pod join $JOIN_TOKEN" fi echo "" fi if [[ "$NO_START" == "true" ]]; then echo "Run ${CYAN}hyperspace start${NC} to begin." echo "Or install as a service: ${CYAN}hyperspace install-service${NC}" else # Try to install as an OS service (auto-restart on crash/reboot) SERVICE_INSTALLED=false if [[ "$OS" == "linux" ]] && command -v systemctl &>/dev/null; then step "Installing as systemd service (auto-restart on crash/reboot)..." # Enable lingering so service survives SSH disconnect loginctl enable-linger "$(whoami)" 2>/dev/null || sudo loginctl enable-linger "$(whoami)" 2>/dev/null || true if "$BIN_DIR/hyperspace" install-service; then SERVICE_INSTALLED=true echo "" ok "Hyperspace is running as a systemd service (auto-restart on crash/reboot)" echo "" echo -e " ${BOLD}Useful commands (open a new terminal):${NC}" echo -e " ${CYAN}systemctl --user status hyperspace${NC} Check status" echo -e " ${CYAN}systemctl --user restart hyperspace${NC} Restart" echo -e " ${CYAN}hyperspace uninstall-service${NC} Remove service" echo "" echo "Following logs (Ctrl+C to detach — agent keeps running)..." echo "" exec journalctl --user -u hyperspace -f else warn "systemd service install failed — falling back to foreground" fi elif [[ "$OS" == "darwin" ]]; then step "Installing as LaunchAgent (auto-restart on crash/reboot)..." if "$BIN_DIR/hyperspace" install-service; then SERVICE_INSTALLED=true echo "" ok "Hyperspace is running as a background service" echo "" echo "Following logs (Ctrl+C to detach — agent keeps running)..." echo "" exec tail -f "${HOME}/.hyperspace/agent.log" fi fi # Start the agent in foreground (exec replaces the shell process). # Works with both `curl | bash` and interactive shells — by the time # we reach here, bash has consumed the entire script from the pipe. if [[ "$SERVICE_INSTALLED" == "false" ]]; then echo "Starting agent..." echo "" echo "Press Ctrl+C to stop." echo -e " Tip: Run ${CYAN}hyperspace install-service${NC} to auto-restart on crash/reboot." echo "" exec "$BIN_DIR/hyperspace" start --api-port "$API_PORT" fi fi