diff --git a/CLAUDE.md b/CLAUDE.md index 79d1f93..4306b84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,8 @@ Rust async web app (Tokio + Axum). Runs as a local chat server with LLM tool-calling and a sub-agent system. > **Never `git commit` unless explicitly asked.** Staging, building, running and testing are fine on your own initiative; creating a commit is not. Do the work, leave it in the working tree, and let the user commit — or ask them to — even when a commit looks like the obvious next step. +> +> **Commit messages must be in English.** ## What this repository is diff --git a/SKALD.md b/SKALD.md index d168e78..866edd0 100644 --- a/SKALD.md +++ b/SKALD.md @@ -1,9 +1,101 @@ # Skald Circle — SKALD +## Installation + +### Stable release + +```sh +curl -fsSL https://builds.skaldagent.net/install.sh | bash +``` + +### Nightly (latest automatic build) + +```sh +curl -fsSL https://builds.skaldagent.net/install-nightly.sh | bash +``` + +### Requirements + +| Required | Notes | +|----------|-------| +| **Docker** | User container sandbox. The installer can install it | +| **Linux (amd64/arm64)** or **macOS ARM64 (Apple Silicon)** | Intel Mac not supported | +| **systemd** (Linux) or **launchd** (macOS) | For running as a service | +| **Python 3** (optional) | For Python MCP servers (Gmail, GCal, GMaps, weather, SSH) and local TTS plugins | +| **Node.js ≥ 18** (optional) | For WhatsApp MCP server | + +The installer checks each requirement and offers to install Docker if missing. +Python and Node.js are optional — the server starts regardless, but certain MCP servers won't work. + +### What it does + +1. Downloads the tarball from `builds.skaldagent.net` +2. Extracts to `~/.local/share/skald-circle/` (or `$SKALD_DIR`) +3. Configures the service (systemd user service / launchd agent) +4. Runs `skald-setup` to create the admin account +5. The server starts at `https://localhost:8443` + +### Uninstallation + +```sh +curl -fsSL https://builds.skaldagent.net/install.sh | bash +# The tarball contains uninstall.sh: +~/.local/share/skald-circle/uninstall.sh +``` + +Or, after installation: `~/.local/share/skald-circle/uninstall.sh` + +### From source + +```sh +git clone https://github.com/.../skald-circle.git +cd skald-circle +cargo build --release +./run.sh +``` + ## Current status New application with agents and chatbots to help families and small groups collaborate, with supervised chat for children and vulnerable people. +## Installer & startup architecture + +``` +install.sh + ├── extracts tarball + ├── creates .venv (inline — does NOT call run.sh) + └── runs skald-setup for interactive config + +systemd service → ExecStart=run.sh + run.sh (supervisor) + ├── creates .venv if it doesn't exist (for local dev) + ├── runs skald-setup (first run only) + └── loop: executes skald binary, restart on exit 255 +``` + +**Rule**: `install.sh` must NEVER call `run.sh`. The venv is created inline in the installer. +`run.sh` is only for the service supervisor or local development. +`skald-setup` is the only setup executable called by install.sh. + +## Bug fix: install.sh stuck on "Setting up Python virtual environment" ✅ + +**Problem**: the installer called `"$INSTALL_DIR/run.sh"` to create the venv. But `run.sh` after the venv runs `skald-setup` and then the server in a loop, hanging the installer forever. + +**Fix**: the venv is now created *inline* in `install.sh` and `install-nightly.sh`, using the same logic as `run.sh` (uv > python3) but without starting the server. + +## Bug fix: "Unit docker.service not found" in user service ✅ + +**Problem**: the systemd user unit had `Requires=docker.service`, but `docker.service` is a system-level unit (not a user unit). `systemctl --user` couldn't find it and refused to start Skald. + +**Fix**: removed `Requires=docker.service` from the user unit template in both install scripts. Kept `After=docker.service` (advisory, doesn't block if the unit isn't found). + +## Bug fix: skald-setup non interattivo con curl | bash ✅ + +**Problem**: `skald-setup` controlla `isatty(0)`, ma con `curl ... | bash` stdin è un pipe, quindi saltava senza chiedere username/password. L'installer arrivava fino in fondo ma senza aver creato l'admin. + +**Fix**: se `IS_INTERACTIVE=false` ma `/dev/tty` esiste, l'installer chiama `skald-setup String { SKALD_SKIP_SETUP=1 never prompt, even on a terminal" .to_string() } - async fn run(mode: Mode) -> Result { // Opening the pool creates `database/system.db` and its schema if absent — // the same call the server makes, so setup and server agree on the layout. + let pool = std::sync::Arc::new( + db::init_system_pool(SYSTEM_DB_PATH) + .await + .context("opening the system database")?, + let db_path = exe_dir.join(SYSTEM_DB_PATH); + + // Opening the pool creates the database and its schema if absent — the same + // call the server makes, so setup and server agree on the layout. + let pool = std::sync::Arc::new( + db::init_system_pool(&db_path) + .await + .context("opening the system database")?, let pool = std::sync::Arc::new( db::init_system_pool(SYSTEM_DB_PATH) .await diff --git a/install-nightly.sh b/install-nightly.sh index 4ae8562..b4688e2 100755 --- a/install-nightly.sh +++ b/install-nightly.sh @@ -7,28 +7,161 @@ # Supports Linux (systemd) and macOS ARM64 (launchd). # Default install dir: ~/.local/share/skald-circle (override with SKALD_DIR). # -# Inspired by: https://hermes-agent.nousresearch.com/install.sh +# If Docker is missing, the installer can optionally install it. set -eu # ── User overrides ──────────────────────────────────────────────────────────── INSTALL_DIR="${SKALD_DIR:-$HOME/.local/share/skald-circle}" +CHANNEL="${SKALD_CHANNEL:-nightly}" + +# ── Detect interactive stdin ────────────────────────────────────────────────── +if [ -t 0 ]; then + IS_INTERACTIVE=true +else + IS_INTERACTIVE=false +fi # ── Colours (if terminal) ───────────────────────────────────────────────────── if [ -t 1 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' + CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' else - RED=''; GREEN=''; YELLOW=''; BOLD=''; NC='' + RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; NC='' fi info() { printf "${GREEN}%s${NC}\n" "$*"; } warn() { printf "${YELLOW}⚠ %s${NC}\n" "$*"; } err() { printf "${RED}✖ %s${NC}\n" "$*"; } header(){ printf "\n${BOLD}%s${NC}\n" "$*"; } +banner(){ printf "\n${CYAN}${BOLD}%s${NC}\n" "$*"; } +# ── Network IP helper ────────────────────────────────────────────────────────── +# Returns the primary non-loopback network IP, or empty string if unavailable. +get_network_ip() { + if command -v ip >/dev/null 2>&1; then + ip route get 1 2>/dev/null | awk '{print $7}' + elif command -v ifconfig >/dev/null 2>&1; then + ifconfig 2>/dev/null | grep -E 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | head -1 + elif command -v hostname >/dev/null 2>&1; then + hostname -I 2>/dev/null | awk '{print $1}' + fi +} + + + +# ── Prompt helpers (work from piped stdin too) ──────────────────────────────── + +prompt_enter() { + local msg="${1:-Press Enter to continue or Ctrl+C to cancel}" + if [ "$IS_INTERACTIVE" = true ]; then + printf "%s" "$msg" >&2 + read -r _ || true + elif (: /dev/null; then + printf "%s" "$msg" >/dev/tty + IFS= read -r _ &2 + read -r ans || ans="y" + elif (: /dev/null; then + printf "%s" "$msg" >/dev/tty + IFS= read -r ans /dev/null 2>&1; then + sudo usermod -aG docker "$USER" + warn "You may need to log out and back in for the docker group to take effect." + fi + info "✔ User added to docker group" + elif [ "$OS" = "darwin" ]; then + if command -v brew >/dev/null 2>&1; then + info "▶ Installing Docker Desktop via Homebrew …" + brew install --cask docker + info "✔ Docker Desktop installed. Open it from Applications to complete setup." + else + err "Homebrew not found. Please install Docker Desktop manually from:" + err " https://docs.docker.com/desktop/setup/install/mac-install/" + err "Then re-run this installer." + exit 1 + fi + fi +} + +ask_install_docker() { + echo "" + header "🐳 Docker" + echo "" + if command -v docker >/dev/null 2>&1 && docker version >/dev/null 2>&1; then + info "✔ Docker is installed and the daemon is running." + return 0 + elif command -v docker >/dev/null 2>&1; then + echo " Docker CLI is present but the daemon is not running." + echo " Please start Docker before starting the server." + echo "" + return 0 + else + warn "Docker is required but not found." + echo "" + echo " Skald uses Docker to run sandboxed user containers." + echo " The server will not start without it." + echo "" + if prompt_yes_no " Install Docker now? [Y/n] "; then + install_docker + echo "" + else + warn "Skipping Docker installation." + echo " You can install it later: https://docs.docker.com/engine/install/" + echo "" + fi + fi +} + +# ── Optional dependency checks ──────────────────────────────────────────────── + +check_optional_deps() { + echo "" + header "🔧 Optional dependencies" + echo "" + + if command -v python3 >/dev/null 2>&1; then + info "✔ Python 3 found ($(python3 --version 2>&1 | head -1))" + else + warn "Python 3 not found — Python MCP servers (Gmail, GCal, GMaps, ...) will not work." + echo " Install it from https://www.python.org/downloads/" + echo "" + fi + + if command -v node >/dev/null 2>&1; then + info "✔ Node.js found ($(node --version 2>&1 | head -1))" + else + warn "Node.js not found — WhatsApp MCP server will not work." + echo " Install it from https://nodejs.org/ (version 18 or later)" + echo "" + fi +} # ── Platform detection ──────────────────────────────────────────────────────── OS="$(uname -s)" @@ -63,18 +196,49 @@ elif [ "$OS" = "darwin" ]; then command -v launchctl >/dev/null 2>&1 || { err "launchctl not found."; exit 1; } fi -# ── Download & extract ──────────────────────────────────────────────────────── +# ── Banner + fetch latest nightly ───────────────────────────────────────────── +banner "╔══════════════════════════════════════════╗" +banner "║ Skald Circle — Nightly Installer ║" +banner "╚══════════════════════════════════════════╝" +echo "" + +info "🔍 Looking up latest nightly build …" BASE_URL="https://builds.skaldagent.net" -TARBALL_URL="${BASE_URL}/nightly/skald-circle-nightly-${OS}-${ARCH}.tar.gz" +case "$CHANNEL" in + nightly) + TARBALL_URL="${BASE_URL}/nightly/skald-circle-nightly-${OS}-${ARCH}.tar.gz" + VERSION="nightly-$(date -u +%Y%m%d)" + DISPLAY_VERSION="nightly" + ;; + *) + err "Unknown channel: $CHANNEL. Use SKALD_CHANNEL=nightly (default)." + exit 1 + ;; +esac -header "📦 Skald Circle — Nightly Installer" +# ── Summary + confirmation ──────────────────────────────────────────────────── echo "" -echo " Platform : ${OS}/${ARCH}" -echo " Install dir : ${INSTALL_DIR}" -echo " Download : ${TARBALL_URL}" +header "Installation summary" +echo "" +echo " What : Skald Circle (${DISPLAY_VERSION})" +echo " Platform : ${OS}/${ARCH}" +echo " Install to : ${INSTALL_DIR}" +echo " Service : $( [ "$OS" = "linux" ] && echo "systemd (user)" || echo "launchd" )" +echo "" +echo " The installer will check for Docker and offer to install it if missing." +echo " Python 3 and Node.js are optional — needed for some MCP servers." echo "" -info "↓ Downloading Skald Circle nightly …" +prompt_enter "Press Enter to continue or Ctrl+C to cancel " + +# ── Docker check & install ──────────────────────────────────────────────────── +ask_install_docker + +# ── Optional dependency check ───────────────────────────────────────────────── +check_optional_deps + +# ── Download & extract ──────────────────────────────────────────────────────── +info "↓ Downloading Skald Circle (${DISPLAY_VERSION}) …" mkdir -p "$INSTALL_DIR" curl -fsSL "$TARBALL_URL" | tar xz -C "$INSTALL_DIR" --strip-components=1 @@ -86,16 +250,26 @@ fi info "✔ Extracted to ${INSTALL_DIR}" # ── Python venv (best-effort) ───────────────────────────────────────────────── +# Create the venv inline instead of calling run.sh (which also launches the +# server and would hang the installer). info "🔧 Setting up Python virtual environment …" -"$INSTALL_DIR/run.sh" >/dev/null 2>&1 || true +VENV_DIR="${INSTALL_DIR}/.venv" +REQUIREMENTS="${INSTALL_DIR}/requirements.txt" -# ── First-run setup (interactive) ───────────────────────────────────────────── -if [ -t 0 ] && [ -x "$INSTALL_DIR/bin/skald-setup" ]; then - header "⚙️ First-time setup" - echo " You will be asked to configure your LLM provider and create an admin user." - echo "" - "$INSTALL_DIR/bin/skald-setup" - echo "" +if [ ! -f "$VENV_DIR/bin/python3" ]; then + if command -v uv >/dev/null 2>&1; then + uv venv "$VENV_DIR" && uv pip install -r "$REQUIREMENTS" \ + && info "✔ Python venv ready (uv)" \ + || warn "Python venv setup failed — Python MCP servers will be unavailable." + elif command -v python3 >/dev/null 2>&1; then + python3 -m venv "$VENV_DIR" && "$VENV_DIR/bin/pip" install -r "$REQUIREMENTS" \ + && info "✔ Python venv ready (pip)" \ + || warn "Python venv setup failed — Python MCP servers will be unavailable." + else + warn "python3 not found — Python MCP servers will be unavailable." + fi +else + info "✔ Python venv already exists" fi # ── Install daemon ──────────────────────────────────────────────────────────── @@ -106,9 +280,9 @@ if [ "$OS" = "linux" ] && [ -z "${NOSYSTEMD:-}" ]; then cat > "$HOME/.config/systemd/user/skald-circle.service" <<- SERVICE [Unit] -Description=Skald Circle (nightly) +Description=Skald Circle (${DISPLAY_VERSION}) Documentation=https://skaldagent.net -After=network.target +After=network.target docker.service [Service] Type=simple @@ -187,9 +361,44 @@ elif [ -n "${NOSYSTEMD:-}" ]; then warn "systemd not available — start manually: ${INSTALL_DIR}/run.sh" fi +# ── Welcome + first-run setup ───────────────────────────────────────────────── echo "" -info "✅ Skald Circle (nightly) installed successfully!" +header "👋 Welcome to Skald Circle!" + +if [ -x "$INSTALL_DIR/bin/skald-setup" ]; then + echo "" + echo " The server is running. Now let's create your admin account." + echo " You'll be asked for a username and password." + echo "" + # skald-setup uses the relative path `database/system.db`, so we cd to the + # install directory first. When running via curl | bash the cwd is ~, which + # would create `~/database/system.db` instead of the correct location. + cd "$INSTALL_DIR" + if [ "$IS_INTERACTIVE" = true ]; then + "bin/skald-setup" + elif (: /dev/null; then + "bin/skald-setup" /dev/null 2>&1; then + ip route get 1 2>/dev/null | awk '{print $7}' + elif command -v ifconfig >/dev/null 2>&1; then + ifconfig 2>/dev/null | grep -E 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | head -1 + elif command -v hostname >/dev/null 2>&1; then + hostname -I 2>/dev/null | awk '{print $1}' + fi +} + + +# ── Prompt helpers (work from piped stdin too) ──────────────────────────────── +# Both read from /dev/tty when stdin is piped (curl | bash), so the user can +# always see and answer. + +prompt_enter() { + local msg="${1:-Press Enter to continue or Ctrl+C to cancel}" + if [ "$IS_INTERACTIVE" = true ]; then + printf "%s" "$msg" >&2 + read -r _ || true + elif (: /dev/null; then + printf "%s" "$msg" >/dev/tty + IFS= read -r _ &2 + read -r ans || ans="y" + elif (: /dev/null; then + printf "%s" "$msg" >/dev/tty + IFS= read -r ans /dev/null 2>&1; then + sudo usermod -aG docker "$USER" + warn "You may need to log out and back in for the docker group to take effect." + fi + info "✔ User added to docker group" + elif [ "$OS" = "darwin" ]; then + if command -v brew >/dev/null 2>&1; then + info "▶ Installing Docker Desktop via Homebrew …" + brew install --cask docker + info "✔ Docker Desktop installed. Open it from Applications to complete setup." + else + err "Homebrew not found. Please install Docker Desktop manually from:" + err " https://docs.docker.com/desktop/setup/install/mac-install/" + err "Then re-run this installer." + exit 1 + fi + fi +} + +ask_install_docker() { + echo "" + header "🐳 Docker" + echo "" + if command -v docker >/dev/null 2>&1 && docker version >/dev/null 2>&1; then + info "✔ Docker is installed and the daemon is running." + return 0 + elif command -v docker >/dev/null 2>&1; then + echo " Docker CLI is present but the daemon is not running." + echo " Please start Docker before starting the server." + echo "" + return 0 + else + warn "Docker is required but not found." + echo "" + echo " Skald uses Docker to run sandboxed user containers." + echo " The server will not start without it." + echo "" + if prompt_yes_no " Install Docker now? [Y/n] "; then + install_docker + echo "" + else + warn "Skipping Docker installation." + echo " You can install it later: https://docs.docker.com/engine/install/" + echo "" + fi + fi +} + + +# ── Optional dependency checks ──────────────────────────────────────────────── +# These are just warnings — the server starts without them, but some MCP servers +# or plugins won't work. + +check_optional_deps() { + echo "" + header "🔧 Optional dependencies" + echo "" + + if command -v python3 >/dev/null 2>&1; then + info "✔ Python 3 found ($(python3 --version 2>&1 | head -1))" + else + warn "Python 3 not found — Python MCP servers (Gmail, GCal, GMaps, ...) will not work." + echo " Install it from https://www.python.org/downloads/" + echo "" + fi + + if command -v node >/dev/null 2>&1; then + info "✔ Node.js found ($(node --version 2>&1 | head -1))" + else + warn "Node.js not found — WhatsApp MCP server will not work." + echo " Install it from https://nodejs.org/ (version 18 or later)" + echo "" + fi +} # ── Platform detection ──────────────────────────────────────────────────────── OS="$(uname -s)" ARCH="$(uname -m)" @@ -63,14 +202,15 @@ elif [ "$OS" = "darwin" ]; then command -v launchctl >/dev/null 2>&1 || { err "launchctl not found."; exit 1; } fi -# ── Fetch latest version ────────────────────────────────────────────────────── -BASE_URL="https://builds.skaldagent.net" -LATEST_URL="${BASE_URL}/releases/LATEST" - -header "📦 Skald Circle — Installer" +# ── Banner + fetch latest version ───────────────────────────────────────────── +banner "╔══════════════════════════════════════════╗" +banner "║ Skald Circle — Installer ║" +banner "╚══════════════════════════════════════════╝" echo "" info "🔍 Looking up latest release …" +BASE_URL="https://builds.skaldagent.net" +LATEST_URL="${BASE_URL}/releases/LATEST" VERSION="$(curl -fsSL "$LATEST_URL" | head -1 | tr -d '[:space:]')" if [ -z "$VERSION" ]; then @@ -81,12 +221,26 @@ fi TARBALL_URL="${BASE_URL}/releases/${VERSION}/skald-circle-${VERSION}-${OS}-${ARCH}.tar.gz" +# ── Summary + confirmation ──────────────────────────────────────────────────── echo "" -echo " Version : ${VERSION}" -echo " Platform : ${OS}/${ARCH}" -echo " Install dir : ${INSTALL_DIR}" -echo " Download : ${TARBALL_URL}" +header "Installation summary" echo "" +echo " What : Skald Circle ${VERSION}" +echo " Platform : ${OS}/${ARCH}" +echo " Install to : ${INSTALL_DIR}" +echo " Service : $( [ "$OS" = "linux" ] && echo "systemd (user)" || echo "launchd" )" +echo "" +echo " The installer will check for Docker and offer to install it if missing." +echo " Python 3 and Node.js are optional — needed for some MCP servers." +echo "" + +prompt_enter "Press Enter to continue or Ctrl+C to cancel " + +# ── Docker check & install ──────────────────────────────────────────────────── +ask_install_docker + +# ── Optional dependency check ───────────────────────────────────────────────── +check_optional_deps # ── Download & extract ──────────────────────────────────────────────────────── info "↓ Downloading Skald Circle ${VERSION} …" @@ -101,16 +255,26 @@ fi info "✔ Extracted to ${INSTALL_DIR}" # ── Python venv (best-effort) ───────────────────────────────────────────────── +# Create the venv inline instead of calling run.sh (which also launches the +# server and would hang the installer). info "🔧 Setting up Python virtual environment …" -"$INSTALL_DIR/run.sh" >/dev/null 2>&1 || true +VENV_DIR="${INSTALL_DIR}/.venv" +REQUIREMENTS="${INSTALL_DIR}/requirements.txt" -# ── First-run setup (interactive) ───────────────────────────────────────────── -if [ -t 0 ] && [ -x "$INSTALL_DIR/bin/skald-setup" ]; then - header "⚙️ First-time setup" - echo " You will be asked to configure your LLM provider and create an admin user." - echo "" - "$INSTALL_DIR/bin/skald-setup" - echo "" +if [ ! -f "$VENV_DIR/bin/python3" ]; then + if command -v uv >/dev/null 2>&1; then + uv venv "$VENV_DIR" && uv pip install -r "$REQUIREMENTS" \ + && info "✔ Python venv ready (uv)" \ + || warn "Python venv setup failed — Python MCP servers will be unavailable." + elif command -v python3 >/dev/null 2>&1; then + python3 -m venv "$VENV_DIR" && "$VENV_DIR/bin/pip" install -r "$REQUIREMENTS" \ + && info "✔ Python venv ready (pip)" \ + || warn "Python venv setup failed — Python MCP servers will be unavailable." + else + warn "python3 not found — Python MCP servers will be unavailable." + fi +else + info "✔ Python venv already exists" fi # ── Install daemon ──────────────────────────────────────────────────────────── @@ -123,7 +287,7 @@ if [ "$OS" = "linux" ] && [ -z "${NOSYSTEMD:-}" ]; then [Unit] Description=Skald Circle (release ${VERSION}) Documentation=https://skaldagent.net -After=network.target +After=network.target docker.service [Service] Type=simple @@ -202,9 +366,44 @@ elif [ -n "${NOSYSTEMD:-}" ]; then warn "systemd not available — start manually: ${INSTALL_DIR}/run.sh" fi +# ── Welcome + first-run setup ───────────────────────────────────────────────── +echo "" +header "👋 Welcome to Skald Circle!" + +if [ -x "$INSTALL_DIR/bin/skald-setup" ]; then + echo "" + echo " The server is running. Now let's create your admin account." + echo " You'll be asked for a username and password." + echo "" + # skald-setup uses the relative path `database/system.db`, so we cd to the + # install directory first. When running via curl | bash the cwd is ~, which + # would create `~/database/system.db` instead of the correct location. + cd "$INSTALL_DIR" + if [ "$IS_INTERACTIVE" = true ]; then + "bin/skald-setup" + elif (: /dev/null; then + "bin/skald-setup"