Version 0.0.1 #2

Merged
dguiducci merged 42 commits from main into release 2026-07-22 14:44:42 +01:00
5 changed files with 564 additions and 51 deletions
Showing only changes of commit 780524a765 - Show all commits
+2
View File
@@ -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. 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. > **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 ## What this repository is
+96 -4
View File
@@ -1,9 +1,101 @@
# Skald Circle — SKALD # 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 ## Current status
New application with agents and chatbots to help families and small groups collaborate, with supervised chat for children and vulnerable people. 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 </dev/tty`.
### Agent icons — completed ✅ ### Agent icons — completed ✅
All 11 agents now have **Vector Paintings** icons (painterly vector, warm and family-friendly), generated via ComfyUI: All 11 agents now have **Vector Paintings** icons (painterly vector, warm and family-friendly), generated via ComfyUI:
@@ -29,7 +121,7 @@ All 11 agents now have **Vector Paintings** icons (painterly vector, warm and fa
- New i18n system (core-api + plugin-mobile-connector + web) - New i18n system (core-api + plugin-mobile-connector + web)
- Configuration system refactoring - Configuration system refactoring
### Auto-build CI/CD ## Auto-build CI/CD (NiPoGi)
Automatic build on NiPoGi with Gitea Actions (native runner v2.1.0): Automatic build on NiPoGi with Gitea Actions (native runner v2.1.0):
@@ -63,7 +155,7 @@ Automatic build on NiPoGi with Gitea Actions (native runner v2.1.0):
- Test release workflow with a PR - Test release workflow with a PR
- Build first macOS ARM64 binary on MacBook, upload to `builds.skaldagent.net` - Build first macOS ARM64 binary on MacBook, upload to `builds.skaldagent.net`
### macOS support ## macOS support
**Supported**: macOS ARM64 (Apple Silicon M1+), Intel not supported. **Supported**: macOS ARM64 (Apple Silicon M1+), Intel not supported.
@@ -75,7 +167,7 @@ Automatic build on NiPoGi with Gitea Actions (native runner v2.1.0):
| **Package script** (`ci/package.sh`) | ✅ | Accepts `--os darwin`, strips best-effort | | **Package script** (`ci/package.sh`) | ✅ | Accepts `--os darwin`, strips best-effort |
| **Binary** | ⏳ Not yet built | Build natively on MacBook, deploy to builds.skaldagent.net | | **Binary** | ⏳ Not yet built | Build natively on MacBook, deploy to builds.skaldagent.net |
#### How to build for macOS (on MacBook) ### How to build for macOS (on MacBook)
```sh ```sh
cargo build --release -p skald-setup -p skald # includes whisper cargo build --release -p skald-setup -p skald # includes whisper
@@ -85,7 +177,7 @@ cargo build --release -p skald-setup -p skald # includes whisper
Upload the resulting `dist/skald-circle-v0.1.0-darwin-arm64.tar.gz` to the NiPoGi's `builds.skaldagent.net/releases/v0.1.0/` directory. Upload the resulting `dist/skald-circle-v0.1.0-darwin-arm64.tar.gz` to the NiPoGi's `builds.skaldagent.net/releases/v0.1.0/` directory.
#### Cross-compilation from NiPoGi (research notes 🧪) ### Cross-compilation from NiPoGi (research notes 🧪)
Cross-compiling for `aarch64-apple-darwin` from the NiPoGi using **zig** + **cargo-zigbuild** was attempted but hit blockers. Cross-compiling for `aarch64-apple-darwin` from the NiPoGi using **zig** + **cargo-zigbuild** was attempted but hit blockers.
+12 -1
View File
@@ -86,10 +86,21 @@ fn usage() -> String {
SKALD_SKIP_SETUP=1 never prompt, even on a terminal" SKALD_SKIP_SETUP=1 never prompt, even on a terminal"
.to_string() .to_string()
} }
async fn run(mode: Mode) -> Result<std::process::ExitCode> { async fn run(mode: Mode) -> Result<std::process::ExitCode> {
// Opening the pool creates `database/system.db` and its schema if absent — // 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. // 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( let pool = std::sync::Arc::new(
db::init_system_pool(SYSTEM_DB_PATH) db::init_system_pool(SYSTEM_DB_PATH)
.await .await
+231 -22
View File
@@ -7,28 +7,161 @@
# Supports Linux (systemd) and macOS ARM64 (launchd). # Supports Linux (systemd) and macOS ARM64 (launchd).
# Default install dir: ~/.local/share/skald-circle (override with SKALD_DIR). # 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 set -eu
# ── User overrides ──────────────────────────────────────────────────────────── # ── User overrides ────────────────────────────────────────────────────────────
INSTALL_DIR="${SKALD_DIR:-$HOME/.local/share/skald-circle}" 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) ───────────────────────────────────────────────────── # ── Colours (if terminal) ─────────────────────────────────────────────────────
if [ -t 1 ]; then if [ -t 1 ]; then
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m' BOLD='\033[1m'
NC='\033[0m' NC='\033[0m'
else else
RED=''; GREEN=''; YELLOW=''; BOLD=''; NC='' RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; NC=''
fi fi
info() { printf "${GREEN}%s${NC}\n" "$*"; } info() { printf "${GREEN}%s${NC}\n" "$*"; }
warn() { printf "${YELLOW}⚠ %s${NC}\n" "$*"; } warn() { printf "${YELLOW}⚠ %s${NC}\n" "$*"; }
err() { printf "${RED}✖ %s${NC}\n" "$*"; } err() { printf "${RED}✖ %s${NC}\n" "$*"; }
header(){ printf "\n${BOLD}%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/tty) 2>/dev/null; then
printf "%s" "$msg" >/dev/tty
IFS= read -r _ </dev/tty || true
fi
}
prompt_yes_no() {
local msg="${1:-Continue? [Y/n]}"
local ans
if [ "$IS_INTERACTIVE" = true ]; then
printf "%s" "$msg" >&2
read -r ans || ans="y"
elif (: </dev/tty) 2>/dev/null; then
printf "%s" "$msg" >/dev/tty
IFS= read -r ans </dev/tty || ans="y"
else
ans="n"
fi
case "$(printf "%s" "$ans" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" in
""|"y"|"yes") return 0 ;;
*) return 1 ;;
esac
}
# ── Docker install helper ─────────────────────────────────────────────────────
install_docker() {
if [ "$OS" = "linux" ]; then
info "▶ Installing Docker via get.docker.com …"
curl -fsSL https://get.docker.com | sh
info "✔ Docker installed"
if command -v usermod >/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 ──────────────────────────────────────────────────────── # ── Platform detection ────────────────────────────────────────────────────────
OS="$(uname -s)" OS="$(uname -s)"
@@ -63,18 +196,49 @@ elif [ "$OS" = "darwin" ]; then
command -v launchctl >/dev/null 2>&1 || { err "launchctl not found."; exit 1; } command -v launchctl >/dev/null 2>&1 || { err "launchctl not found."; exit 1; }
fi 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" 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 ""
header "Installation summary"
echo ""
echo " What : Skald Circle (${DISPLAY_VERSION})"
echo " Platform : ${OS}/${ARCH}" echo " Platform : ${OS}/${ARCH}"
echo " Install dir : ${INSTALL_DIR}" echo " Install to : ${INSTALL_DIR}"
echo " Download : ${TARBALL_URL}" 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 "" 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" mkdir -p "$INSTALL_DIR"
curl -fsSL "$TARBALL_URL" | tar xz -C "$INSTALL_DIR" --strip-components=1 curl -fsSL "$TARBALL_URL" | tar xz -C "$INSTALL_DIR" --strip-components=1
@@ -86,16 +250,26 @@ fi
info "✔ Extracted to ${INSTALL_DIR}" info "✔ Extracted to ${INSTALL_DIR}"
# ── Python venv (best-effort) ───────────────────────────────────────────────── # ── 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 …" 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 [ ! -f "$VENV_DIR/bin/python3" ]; then
if [ -t 0 ] && [ -x "$INSTALL_DIR/bin/skald-setup" ]; then if command -v uv >/dev/null 2>&1; then
header "⚙️ First-time setup" uv venv "$VENV_DIR" && uv pip install -r "$REQUIREMENTS" \
echo " You will be asked to configure your LLM provider and create an admin user." && info "✔ Python venv ready (uv)" \
echo "" || warn "Python venv setup failed — Python MCP servers will be unavailable."
"$INSTALL_DIR/bin/skald-setup" elif command -v python3 >/dev/null 2>&1; then
echo "" 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 fi
# ── Install daemon ──────────────────────────────────────────────────────────── # ── Install daemon ────────────────────────────────────────────────────────────
@@ -106,9 +280,9 @@ if [ "$OS" = "linux" ] && [ -z "${NOSYSTEMD:-}" ]; then
cat > "$HOME/.config/systemd/user/skald-circle.service" <<- SERVICE cat > "$HOME/.config/systemd/user/skald-circle.service" <<- SERVICE
[Unit] [Unit]
Description=Skald Circle (nightly) Description=Skald Circle (${DISPLAY_VERSION})
Documentation=https://skaldagent.net Documentation=https://skaldagent.net
After=network.target After=network.target docker.service
[Service] [Service]
Type=simple Type=simple
@@ -187,9 +361,44 @@ elif [ -n "${NOSYSTEMD:-}" ]; then
warn "systemd not available — start manually: ${INSTALL_DIR}/run.sh" warn "systemd not available — start manually: ${INSTALL_DIR}/run.sh"
fi fi
# ── Welcome + first-run setup ─────────────────────────────────────────────────
echo "" 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/tty) 2>/dev/null; then
"bin/skald-setup" </dev/tty
else
echo " From a terminal, run:"
echo " cd ${INSTALL_DIR} && ./bin/skald-setup"
fi
echo ""
fi
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
info "✅ Skald Circle (${DISPLAY_VERSION}) installed successfully!"
echo ""
echo " Server : ${INSTALL_DIR}/run.sh"
echo " Binary : ${INSTALL_DIR}/bin/skald"
echo " Setup : ${INSTALL_DIR}/bin/skald-setup"
NET_IP="$(get_network_ip)"
ADMIN_URL="${NET_IP:+http://${NET_IP}:9000}"
echo " Admin console: ${ADMIN_URL:-http://localhost:9000} (on the machine, or use the IP above from another device)"
echo " Server IP: ${NET_IP:-localhost} (network address if available)"
echo ""
echo ""
echo " Start: $( [ "$OS" = "linux" ] && echo "systemctl --user start skald-circle" || echo "launchctl start com.skald.circle" )"
echo " Stop: $( [ "$OS" = "linux" ] && echo "systemctl --user stop skald-circle" || echo "launchctl stop com.skald.circle" )"
echo " Logs: $( [ "$OS" = "linux" ] && echo "journalctl --user -u skald-circle -f" || echo "tail -f ${INSTALL_DIR}/logs/stdout.log" )"
echo "" echo ""
echo " ${INSTALL_DIR}/run.sh"
echo " ${INSTALL_DIR}/bin/skald"
echo " ${INSTALL_DIR}/bin/skald-setup"
+221 -22
View File
@@ -7,29 +7,168 @@
# Supports Linux (systemd) and macOS ARM64 (launchd). # Supports Linux (systemd) and macOS ARM64 (launchd).
# Default install dir: ~/.local/share/skald-circle (override with SKALD_DIR). # 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 set -eu
# ── User overrides ──────────────────────────────────────────────────────────── # ── User overrides ────────────────────────────────────────────────────────────
INSTALL_DIR="${SKALD_DIR:-$HOME/.local/share/skald-circle}" INSTALL_DIR="${SKALD_DIR:-$HOME/.local/share/skald-circle}"
# ── Detect interactive stdin ──────────────────────────────────────────────────
# We need this early: the confirmation prompt reads from /dev/tty when stdin is
# piped (curl | bash), but we only show it when a terminal is actually available.
if [ -t 0 ]; then
IS_INTERACTIVE=true
else
IS_INTERACTIVE=false
fi
# ── Colours (if terminal) ───────────────────────────────────────────────────── # ── Colours (if terminal) ─────────────────────────────────────────────────────
if [ -t 1 ]; then if [ -t 1 ]; then
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m' BOLD='\033[1m'
NC='\033[0m' NC='\033[0m'
else else
RED=''; GREEN=''; YELLOW=''; BOLD=''; NC='' RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; NC=''
fi fi
info() { printf "${GREEN}%s${NC}\n" "$*"; } info() { printf "${GREEN}%s${NC}\n" "$*"; }
warn() { printf "${YELLOW}⚠ %s${NC}\n" "$*"; } warn() { printf "${YELLOW}⚠ %s${NC}\n" "$*"; }
err() { printf "${RED}✖ %s${NC}\n" "$*"; } err() { printf "${RED}✖ %s${NC}\n" "$*"; }
header(){ printf "\n${BOLD}%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) ────────────────────────────────
# 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/tty) 2>/dev/null; then
printf "%s" "$msg" >/dev/tty
IFS= read -r _ </dev/tty || true
fi
}
prompt_yes_no() {
local msg="${1:-Continue? [Y/n]}"
local ans
if [ "$IS_INTERACTIVE" = true ]; then
printf "%s" "$msg" >&2
read -r ans || ans="y"
elif (: </dev/tty) 2>/dev/null; then
printf "%s" "$msg" >/dev/tty
IFS= read -r ans </dev/tty || ans="y"
else
ans="n"
fi
case "$(printf "%s" "$ans" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" in
""|"y"|"yes") return 0 ;;
*) return 1 ;;
esac
}
# ── Docker install helper ─────────────────────────────────────────────────────
install_docker() {
if [ "$OS" = "linux" ]; then
info "▶ Installing Docker via get.docker.com …"
curl -fsSL https://get.docker.com | sh
info "✔ Docker installed"
# Add current user to docker group so they don't need sudo for every command
if command -v usermod >/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 ──────────────────────────────────────────────────────── # ── Platform detection ────────────────────────────────────────────────────────
OS="$(uname -s)" OS="$(uname -s)"
ARCH="$(uname -m)" ARCH="$(uname -m)"
@@ -63,14 +202,15 @@ elif [ "$OS" = "darwin" ]; then
command -v launchctl >/dev/null 2>&1 || { err "launchctl not found."; exit 1; } command -v launchctl >/dev/null 2>&1 || { err "launchctl not found."; exit 1; }
fi fi
# ── Fetch latest version ────────────────────────────────────────────────────── # ── Banner + fetch latest version ─────────────────────────────────────────────
BASE_URL="https://builds.skaldagent.net" banner "╔══════════════════════════════════════════╗"
LATEST_URL="${BASE_URL}/releases/LATEST" banner "║ Skald Circle — Installer ║"
banner "╚══════════════════════════════════════════╝"
header "📦 Skald Circle — Installer"
echo "" echo ""
info "🔍 Looking up latest release …" 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:]')" VERSION="$(curl -fsSL "$LATEST_URL" | head -1 | tr -d '[:space:]')"
if [ -z "$VERSION" ]; then if [ -z "$VERSION" ]; then
@@ -81,12 +221,26 @@ fi
TARBALL_URL="${BASE_URL}/releases/${VERSION}/skald-circle-${VERSION}-${OS}-${ARCH}.tar.gz" TARBALL_URL="${BASE_URL}/releases/${VERSION}/skald-circle-${VERSION}-${OS}-${ARCH}.tar.gz"
# ── Summary + confirmation ────────────────────────────────────────────────────
echo "" echo ""
echo " Version : ${VERSION}" header "Installation summary"
echo ""
echo " What : Skald Circle ${VERSION}"
echo " Platform : ${OS}/${ARCH}" echo " Platform : ${OS}/${ARCH}"
echo " Install dir : ${INSTALL_DIR}" echo " Install to : ${INSTALL_DIR}"
echo " Download : ${TARBALL_URL}" echo " Service : $( [ "$OS" = "linux" ] && echo "systemd (user)" || echo "launchd" )"
echo "" 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 ──────────────────────────────────────────────────────── # ── Download & extract ────────────────────────────────────────────────────────
info "↓ Downloading Skald Circle ${VERSION}" info "↓ Downloading Skald Circle ${VERSION}"
@@ -101,16 +255,26 @@ fi
info "✔ Extracted to ${INSTALL_DIR}" info "✔ Extracted to ${INSTALL_DIR}"
# ── Python venv (best-effort) ───────────────────────────────────────────────── # ── 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 …" 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 [ ! -f "$VENV_DIR/bin/python3" ]; then
if [ -t 0 ] && [ -x "$INSTALL_DIR/bin/skald-setup" ]; then if command -v uv >/dev/null 2>&1; then
header "⚙️ First-time setup" uv venv "$VENV_DIR" && uv pip install -r "$REQUIREMENTS" \
echo " You will be asked to configure your LLM provider and create an admin user." && info "✔ Python venv ready (uv)" \
echo "" || warn "Python venv setup failed — Python MCP servers will be unavailable."
"$INSTALL_DIR/bin/skald-setup" elif command -v python3 >/dev/null 2>&1; then
echo "" 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 fi
# ── Install daemon ──────────────────────────────────────────────────────────── # ── Install daemon ────────────────────────────────────────────────────────────
@@ -123,7 +287,7 @@ if [ "$OS" = "linux" ] && [ -z "${NOSYSTEMD:-}" ]; then
[Unit] [Unit]
Description=Skald Circle (release ${VERSION}) Description=Skald Circle (release ${VERSION})
Documentation=https://skaldagent.net Documentation=https://skaldagent.net
After=network.target After=network.target docker.service
[Service] [Service]
Type=simple Type=simple
@@ -202,9 +366,44 @@ elif [ -n "${NOSYSTEMD:-}" ]; then
warn "systemd not available — start manually: ${INSTALL_DIR}/run.sh" warn "systemd not available — start manually: ${INSTALL_DIR}/run.sh"
fi 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/tty) 2>/dev/null; then
"bin/skald-setup" </dev/tty
else
echo " From a terminal, run:"
echo " cd ${INSTALL_DIR} && ./bin/skald-setup"
fi
echo ""
fi
# ── Done ──────────────────────────────────────────────────────────────────────
echo "" echo ""
info "✅ Skald Circle ${VERSION} installed successfully!" info "✅ Skald Circle ${VERSION} installed successfully!"
echo "" echo ""
echo " ${INSTALL_DIR}/run.sh" echo " Server : ${INSTALL_DIR}/run.sh"
echo " ${INSTALL_DIR}/bin/skald" echo " Binary : ${INSTALL_DIR}/bin/skald"
echo " ${INSTALL_DIR}/bin/skald-setup" echo " Setup : ${INSTALL_DIR}/bin/skald-setup"
NET_IP="$(get_network_ip)"
ADMIN_URL="${NET_IP:+http://${NET_IP}:9000}"
echo " Admin console: ${ADMIN_URL:-http://localhost:9000} (on the machine, or use the IP above from another device)"
echo " Server IP: ${NET_IP:-localhost} (network address if available)"
echo ""
echo ""
echo " Start: $( [ "$OS" = "linux" ] && echo "systemctl --user start skald-circle" || echo "launchctl start com.skald.circle" )"
echo " Stop: $( [ "$OS" = "linux" ] && echo "systemctl --user stop skald-circle" || echo "launchctl stop com.skald.circle" )"
echo " Logs: $( [ "$OS" = "linux" ] && echo "journalctl --user -u skald-circle -f" || echo "tail -f ${INSTALL_DIR}/logs/stdout.log" )"
echo ""