#!/usr/bin/env bash set -euo pipefail # ============================================================================= # LocalTaskClaw — Install Wizard # Usage: curl -fsSL https://raw.githubusercontent.com/vakovalskii/LocalTaskClaw/main/install.sh | bash # ============================================================================= # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' REPO_URL="https://github.com/vakovalskii/LocalTaskClaw" SPINNER_PID="" # --- Helpers ----------------------------------------------------------------- print_header() { echo "" echo -e "${BOLD}${CYAN}╔═══════════════════════════════════════════════╗${NC}" echo -e "${BOLD}${CYAN}║ 🤖 LocalTaskClaw Installer ║${NC}" echo -e "${BOLD}${CYAN}║ Персональный ИИ-агент за 5 минут ║${NC}" echo -e "${BOLD}${CYAN}╚═══════════════════════════════════════════════╝${NC}" echo "" } info() { echo -e "${BLUE}[→]${NC} $*"; } success() { echo -e "${GREEN}[✓]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } error() { echo -e "${RED}[✗]${NC} $*"; } step() { echo -e "\n${BOLD}${CYAN}── $* ${NC}"; } dim() { echo -e "${DIM}$*${NC}"; } sanitize() { # Strip control characters (CR, LF, tab, and other non-printable chars) that # can sneak in from copy-paste and break YAML/env files local val="$1" val="${val//[$'\r\n\t']}" # remove CR, LF, tab val="${val//[[:cntrl:]]}" # remove any remaining control chars printf '%s' "$val" } prompt() { local var_name="$1" message="$2" default="${3:-}" input if [[ -n "$default" ]]; then echo -ne "${BOLD}$message${NC} ${YELLOW}[${default}]${NC}: " >/dev/tty else echo -ne "${BOLD}$message${NC}: " >/dev/tty fi read -r input /dev/tty read -rs input /dev/tty input="$(sanitize "$input")" # Show masked preview so user knows something was entered local len=${#input} if [[ $len -gt 8 ]]; then local show="${input:0:4}$(printf '%*s' $((len - 8)) '' | tr ' ' '*')${input: -4}" echo -e " ${YELLOW}→ Получено: ${show}${NC}" >/dev/tty elif [[ $len -gt 0 ]]; then echo -e " ${YELLOW}→ Получено: $(printf '%*s' "$len" '' | tr ' ' '*')${NC}" >/dev/tty else echo -e " ${RED}→ Пусто! Ничего не введено${NC}" >/dev/tty fi printf -v "$var_name" '%s' "$input" } spinner_start() { local msg="$1" echo -ne "${BLUE}[…]${NC} $msg " (while true; do for c in '⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏'; do echo -ne "\r${BLUE}[$c]${NC} $msg " sleep 0.1 done done) & SPINNER_PID=$! disown "$SPINNER_PID" 2>/dev/null || true } spinner_stop() { if [[ -n "$SPINNER_PID" ]]; then kill "$SPINNER_PID" 2>/dev/null || true wait "$SPINNER_PID" 2>/dev/null || true SPINNER_PID="" echo -ne "\r\033[K" fi } write_secret() { printf '%s' "$2" > "$1" chmod 600 "$1" } generate_secret() { if command -v openssl &>/dev/null; then openssl rand -hex 24 else python3 -c "import secrets; print(secrets.token_hex(24))" fi } cleanup() { spinner_stop; echo "" warn "Установка прервана."; exit 130 } trap cleanup INT TERM # ============================================================================= # HEADER # ============================================================================= print_header # ============================================================================= # ШАГ 1 — ВЫБОР РЕЖИМА ИЗОЛЯЦИИ # ============================================================================= step "Шаг 1 — Как установить агента?" echo "" echo -e " ${BOLD}1)${NC} 🐳 ${BOLD}Docker${NC} — с изоляцией ${CYAN}(рекомендуем для сервера)${NC}" dim " Агент запускается в контейнерах. Доступ только к выделенному тому." echo "" echo -e " ${BOLD}2)${NC} ⚡ ${BOLD}Процессы${NC} — без изоляции, напрямую" dim " Агент работает как Python-процесс. Видит всю файловую систему." echo "" echo -e " ${BOLD}3)${NC} 📁 ${BOLD}Ограничить папкой агента${NC} — процессы + sandbox" dim " Как вариант 2, но агент заперт в ~/.localtaskclaw/workspace." echo "" prompt INSTALL_MODE "Выбор" "1" case "$INSTALL_MODE" in 2) MODE_NAME="native" ;; 3) MODE_NAME="restricted" ;; *) MODE_NAME="docker" ;; esac success "Режим: ${BOLD}${MODE_NAME}${NC}" # ============================================================================= # ШАГ 2 — ПРОВЕРКА ЗАВИСИМОСТЕЙ # ============================================================================= step "Шаг 2 — Проверка зависимостей" # python3 нужен всегда (для поллинга Telegram) if ! command -v python3 &>/dev/null; then error "python3 не найден. Установи: brew install python3 / apt install python3" exit 1 fi success "Python $(python3 --version | awk '{print $2}')" if ! command -v curl &>/dev/null; then error "curl не найден" exit 1 fi success "curl найден" if [[ "$MODE_NAME" == "docker" ]]; then if ! command -v docker &>/dev/null; then error "Docker не найден → https://docs.docker.com/engine/install/" exit 1 fi if ! docker info &>/dev/null; then error "Docker daemon не запущен. Запусти: sudo systemctl start docker" exit 1 fi success "Docker $(docker --version | awk '{print $3}' | tr -d ',')" if docker compose version &>/dev/null 2>&1; then COMPOSE_CMD="docker compose" elif command -v docker-compose &>/dev/null; then COMPOSE_CMD="docker-compose" else error "Docker Compose не найден → https://docs.docker.com/compose/install/" exit 1 fi success "Compose: $COMPOSE_CMD" else # Native / restricted — нужен git и pip if ! command -v git &>/dev/null; then error "git не найден. Установи: brew install git / apt install git" exit 1 fi success "git $(git --version | awk '{print $3}')" if ! python3 -m pip --version &>/dev/null 2>&1; then error "pip не найден. Запусти: python3 -m ensurepip --upgrade" exit 1 fi success "pip найден" fi # ============================================================================= # ШАГ 3 — TELEGRAM БОТ # ============================================================================= step "Шаг 3 — Telegram бот" TG_TOKEN="" BOT_USERNAME="" OWNER_ID="" OWNER_NAME="" while true; do prompt_secret TG_TOKEN "Токен бота (от @BotFather)" # trim whitespace/CR that can sneak in from copy-paste TG_TOKEN="${TG_TOKEN//[$'\r\n\t ']}" if ! echo "$TG_TOKEN" | grep -qE '^[0-9]+:[A-Za-z0-9_-]{30,}$'; then error "Неверный формат. Должно быть: 1234567890:ABCdef..." info "Совет: скопируй токен из BotFather без лишних пробелов" continue fi spinner_start "Проверяю токен..." BOT_INFO=$(curl -s --max-time 10 "https://api.telegram.org/bot${TG_TOKEN}/getMe" 2>/dev/null || echo '{"ok":false}') spinner_stop BOT_OK=$(python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print('yes' if d.get('ok') else 'no')" <<< "$BOT_INFO") if [[ "$BOT_OK" == "yes" ]]; then BOT_USERNAME=$(python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d['result']['username'])" <<< "$BOT_INFO") success "Бот: @${BOLD}${BOT_USERNAME}${NC}" break else BOT_ERR=$(python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('description','unknown'))" <<< "$BOT_INFO") error "Ошибка: $BOT_ERR" fi done # Сбросить webhook если установлен — иначе getUpdates не работает spinner_start "Сбрасываю webhook (если есть)..." curl -s --max-time 5 "https://api.telegram.org/bot${TG_TOKEN}/deleteWebhook?drop_pending_updates=false" &>/dev/null || true spinner_stop echo "" info "Открой бота ${BOLD}@${BOT_USERNAME}${NC} в Telegram и нажми ${BOLD}/start${NC}" info "Жду 120 сек... Или введи Telegram ID вручную прямо сейчас." echo "" RESULT=$(python3 - "$TG_TOKEN" <<'PYEOF' import sys, json, urllib.request, time token = sys.argv[1] offset = 0 # Skip already-seen updates try: url = f"https://api.telegram.org/bot{token}/getUpdates?limit=1&offset=-1&timeout=0" r = urllib.request.urlopen(url, timeout=8) data = json.loads(r.read()) if not data.get("ok"): # getUpdates forbidden — webhook active? Try anyway pass updates = data.get("result", []) if updates: offset = updates[-1]["update_id"] + 1 except Exception as e: sys.stderr.write(f"offset init error: {e}\n") deadline = time.time() + 120 dots = 0 while time.time() < deadline: try: # long-poll 5s, socket timeout 10s url = (f"https://api.telegram.org/bot{token}" f"/getUpdates?timeout=5&offset={offset}" f"&allowed_updates=%5B%22message%22%5D") r = urllib.request.urlopen(url, timeout=10) data = json.loads(r.read()) for upd in data.get("result", []): offset = upd["update_id"] + 1 msg = upd.get("message", {}) text = msg.get("text", "") if text.startswith("/start") or text: uid = msg.get("from", {}).get("id", "") name = msg.get("from", {}).get("first_name", "User") if uid: print(f"FOUND:{uid}:{name}") sys.exit(0) except Exception as e: sys.stderr.write(f"poll error: {e}\n") time.sleep(2) continue dots = (dots + 1) % 4 print(f"\r Ожидание /start{'.' * dots} ", end="", flush=True) print("\rTIMEOUT" + " " * 40) sys.exit(1) PYEOF ) || true if echo "$RESULT" | grep -q "^FOUND:"; then OWNER_ID=$(echo "$RESULT" | grep "^FOUND:" | head -1 | cut -d: -f2) OWNER_NAME=$(echo "$RESULT" | grep "^FOUND:" | head -1 | cut -d: -f3-) success "Владелец: ${BOLD}${OWNER_NAME}${NC} (ID: ${BOLD}${OWNER_ID}${NC})" else echo "" warn "Не поймал /start автоматически." echo "" echo -e " Найди свой Telegram ID на: ${CYAN}https://t.me/userinfobot${NC}" echo -e " Или напиши ${BOLD}@userinfobot${NC} в Telegram — он ответит твоим ID." echo "" prompt OWNER_ID "Telegram ID владельца" "" OWNER_NAME="Owner" fi # ============================================================================= # ШАГ 4 — LLM МОДЕЛЬ # ============================================================================= step "Шаг 4 — Языковая модель" echo -e " ${BOLD}1)${NC} Ollama — локальная модель на этом сервере ${CYAN}(рекомендуем)${NC}" echo -e " ${BOLD}2)${NC} Внешний API — OpenAI / свой сервер / облако" echo "" prompt MODEL_SOURCE "Выбор" "1" LLM_BASE_URL="" LLM_API_KEY="" MODEL_NAME="" USE_OLLAMA="" if [[ "$MODEL_SOURCE" == "1" ]]; then USE_OLLAMA="yes" # Hardware detection TOTAL_RAM_GB=0 GPU_VRAM_GB=0 TOTAL_RAM_KB=$(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}' || echo "0") TOTAL_RAM_GB=$(( TOTAL_RAM_KB / 1024 / 1024 )) if command -v nvidia-smi &>/dev/null; then GPU_VRAM_MB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | head -1 | tr -d ' ' || echo "0") GPU_VRAM_GB=$(( GPU_VRAM_MB / 1024 )) fi AVAILABLE_GB=$TOTAL_RAM_GB [[ "$GPU_VRAM_GB" -gt 4 ]] && AVAILABLE_GB=$GPU_VRAM_GB info "RAM: ${BOLD}${TOTAL_RAM_GB}GB${NC} GPU VRAM: ${BOLD}${GPU_VRAM_GB}GB${NC}" echo "" declare -A MODEL_OPTIONS OPT_NUM=1 add_model_option() { local min_gb="$1" name="$2" tag="$3" size="$4" note="$5" if [[ "$AVAILABLE_GB" -ge "$min_gb" ]]; then echo -e " ${BOLD}${OPT_NUM})${NC} ${name}:${tag} ${YELLOW}(${size})${NC} — ${note}" MODEL_OPTIONS["$OPT_NUM"]="${name}:${tag}" OPT_NUM=$((OPT_NUM + 1)) fi } add_model_option 3 "qwen2.5" "3b" "2.0GB" "минимум RAM" add_model_option 6 "qwen2.5" "7b" "4.7GB" "★ отличный tool use" add_model_option 10 "llama3.1" "8b" "4.9GB" "Meta, хорош для кода" add_model_option 12 "qwen2.5" "14b" "9.0GB" "★★ сильный reasoning" add_model_option 22 "qwen2.5" "32b" "19GB" "★★★ лучший до 32GB" add_model_option 22 "deepseek-r1" "14b" "9.0GB" "reasoning + цепочка мыслей" add_model_option 50 "qwen2.5" "72b" "47GB" "топ open-source" CUSTOM_OPT=$OPT_NUM echo -e " ${BOLD}${OPT_NUM})${NC} Ввести своё название" echo "" prompt OLLAMA_CHOICE "Выбор" "1" if [[ "$OLLAMA_CHOICE" == "$CUSTOM_OPT" ]]; then prompt MODEL_NAME "Название модели (например: mistral:7b)" "" elif [[ -n "${MODEL_OPTIONS[$OLLAMA_CHOICE]:-}" ]]; then MODEL_NAME="${MODEL_OPTIONS[$OLLAMA_CHOICE]}" else MODEL_NAME="${MODEL_OPTIONS[1]:-qwen2.5:7b}" fi success "Выбрана модель: ${BOLD}${MODEL_NAME}${NC}" # Install Ollama if command -v ollama &>/dev/null; then success "Ollama уже установлен" else info "Устанавливаю Ollama..." spinner_start "Установка Ollama..." if curl -fsSL https://ollama.com/install.sh | sh > /tmp/ollama_install.log 2>&1; then spinner_stop; success "Ollama установлен" else spinner_stop; error "Ошибка установки Ollama:"; tail -10 /tmp/ollama_install.log; exit 1 fi fi # Start Ollama if ! curl -s --max-time 2 http://localhost:11434/ &>/dev/null; then info "Запускаю Ollama..." if command -v systemctl &>/dev/null && systemctl is-enabled ollama &>/dev/null 2>&1; then systemctl start ollama else nohup ollama serve > /tmp/ollama.log 2>&1 & sleep 3 fi fi # Pull model echo "" info "Скачиваю ${BOLD}${MODEL_NAME}${NC}..." if ! ollama pull "$MODEL_NAME"; then error "Ошибка скачивания. Попробуй вручную: ollama pull ${MODEL_NAME}" exit 1 fi success "Модель скачана" if [[ "$MODE_NAME" == "docker" ]]; then LLM_BASE_URL="http://host.docker.internal:11434/v1" else LLM_BASE_URL="http://localhost:11434/v1" fi LLM_API_KEY="ollama" else # External API info "Примеры URL: https://api.openai.com/v1 / http://your-server:44334/v1" echo "" prompt LLM_BASE_URL "Base URL" "" prompt_secret LLM_API_KEY "API ключ" prompt MODEL_NAME "Название модели" "gpt-4o" success "Модель: ${BOLD}${MODEL_NAME}${NC}" fi # ============================================================================= # ШАГ 5 — ВЕБ-ПОИСК # ============================================================================= step "Шаг 5 — Веб-поиск" BRAVE_KEY="" success "Веб-поиск через DuckDuckGo включён по умолчанию (Brave API опционально — настройки)" # ============================================================================= # ШАГ 6 — ПОДТВЕРЖДЕНИЕ # ============================================================================= step "Шаг 6 — Проверка" echo "" # Helper: pad a string to N visible chars (accounts for multibyte UTF-8) _pad() { local str="$1" width="$2" local visible_len=$(echo -n "$str" | LC_ALL=en_US.UTF-8 wc -m | tr -d ' ') local pad=$((width - visible_len)) [[ $pad -lt 0 ]] && pad=0 printf '%s%*s' "$str" "$pad" "" } SHORT_URL="$LLM_BASE_URL" local_search="DuckDuckGo (+ Brave)" echo -e "${BOLD}┌──────────────────────────────────────────────────────────┐${NC}" echo -e "${BOLD}│${NC} $(_pad "Режим" 16) $(_pad "$MODE_NAME" 38) ${BOLD}│${NC}" echo -e "${BOLD}│${NC} $(_pad "Telegram бот" 16) $(_pad "@$BOT_USERNAME" 38) ${BOLD}│${NC}" echo -e "${BOLD}│${NC} $(_pad "Владелец" 16) $(_pad "$OWNER_NAME ($OWNER_ID)" 38) ${BOLD}│${NC}" echo -e "${BOLD}│${NC} $(_pad "Модель" 16) $(_pad "$MODEL_NAME" 38) ${BOLD}│${NC}" echo -e "${BOLD}│${NC} $(_pad "LLM URL" 16) $(_pad "${SHORT_URL:0:38}" 38) ${BOLD}│${NC}" echo -e "${BOLD}│${NC} $(_pad "Веб-поиск" 16) $(_pad "$local_search" 38) ${BOLD}│${NC}" echo -e "${BOLD}└──────────────────────────────────────────────────────────┘${NC}" echo "" echo -ne "${BOLD}Запустить установку? [Y/n]${NC}: " >/dev/tty read -r CONFIRM /tmp/localtaskclaw_clone.log 2>&1; then spinner_stop; error "Ошибка клонирования:"; tail -5 /tmp/localtaskclaw_clone.log; exit 1 fi spinner_stop; success "Код скачан" fi # Write secrets as env files cat > "$INSTALL_DIR/secrets/core.env" << ENV MODEL=${MODEL_NAME} LLM_BASE_URL=${LLM_BASE_URL} LLM_API_KEY=${LLM_API_KEY} BOT_TOKEN=${TG_TOKEN} OWNER_ID=${OWNER_ID} API_SECRET=${API_SECRET} WORKSPACE=/data/workspace DB_PATH=/data/localtaskclaw.db BRAVE_API_KEY=${BRAVE_KEY:-} MAX_ITERATIONS=20 COMMAND_TIMEOUT=60 MAX_TOKENS=4096 API_PORT=11387 ENV chmod 600 "$INSTALL_DIR/secrets/core.env" cat > "$INSTALL_DIR/secrets/bot.env" << ENV CORE_URL=http://core:11387 BOT_TOKEN=${TG_TOKEN} API_SECRET=${API_SECRET} OWNER_ID=${OWNER_ID} ENV chmod 600 "$INSTALL_DIR/secrets/bot.env" # Also save api_secret standalone for ltc CLI write_secret "$INSTALL_DIR/secrets/api_secret.txt" "$API_SECRET" cat > "$INSTALL_DIR/docker-compose.yml" << COMPOSE services: core: build: context: ./app dockerfile: Dockerfile container_name: localtaskclaw-core restart: unless-stopped env_file: ./secrets/core.env environment: WORKSPACE: /data/workspace DB_PATH: /data/localtaskclaw.db API_PORT: "11387" volumes: - ./data:/data ports: - "11387:11387" healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:11387/health"] interval: 30s timeout: 5s retries: 3 start_period: 20s bot: build: context: ./app/bot dockerfile: Dockerfile container_name: localtaskclaw-bot restart: unless-stopped env_file: ./secrets/bot.env environment: CORE_URL: http://core:11387 depends_on: core: condition: service_healthy COMPOSE success "Конфиг: ${BOLD}$INSTALL_DIR${NC}" info "Собираю и запускаю контейнеры..." cd "$INSTALL_DIR" spinner_start "docker compose build + up..." if ! $COMPOSE_CMD up -d --build > /tmp/localtaskclaw_up.log 2>&1; then spinner_stop error "Ошибка запуска:"; tail -20 /tmp/localtaskclaw_up.log; exit 1 fi spinner_stop; success "Контейнеры запущены" # Health check spinner_start "Жду готовности..." HEALTHY=false for i in $(seq 1 30); do if curl -s --max-time 2 http://localhost:11387/health &>/dev/null; then HEALTHY=true; break fi sleep 2 done spinner_stop [[ "$HEALTHY" == "true" ]] && success "Сервис готов!" || warn "Сервис ещё загружается..." ADMIN_URL="http://localhost:11387/admin" MANAGE_INFO="Логи: ${BOLD}ltc logs${NC} Стоп: ${BOLD}ltc stop${NC} Обновить: ${BOLD}ltc update${NC}" fi # ============================================================================= # УСТАНОВКА — NATIVE / RESTRICTED # ============================================================================= if [[ "$MODE_NAME" == "native" || "$MODE_NAME" == "restricted" ]]; then INSTALL_DIR="$HOME/.localtaskclaw" WORKSPACE_DIR="$INSTALL_DIR/workspace" DB_PATH="$HOME/.localtaskclaw/localtaskclaw.db" VENV_DIR="$INSTALL_DIR/venv" CODE_DIR="$INSTALL_DIR/app" mkdir -p "$INSTALL_DIR" "$WORKSPACE_DIR" "$HOME/.localtaskclaw" # Clone repo if [[ -d "$CODE_DIR/.git" ]]; then info "Обновляю код..." git -C "$CODE_DIR" pull --rebase --quiet else info "Клонирую репозиторий..." spinner_start "git clone..." if ! git clone --quiet "$REPO_URL" "$CODE_DIR" > /tmp/localtaskclaw_clone.log 2>&1; then spinner_stop; error "Ошибка клонирования:"; tail -5 /tmp/localtaskclaw_clone.log; exit 1 fi spinner_stop; success "Код скачан" fi # Virtual environment if [[ ! -d "$VENV_DIR" ]]; then info "Создаю виртуальное окружение..." python3 -m venv "$VENV_DIR" fi info "Устанавливаю зависимости..." spinner_start "pip install..." if ! "$VENV_DIR/bin/pip" install -q -r "$CODE_DIR/core/requirements.txt" > /tmp/localtaskclaw_pip.log 2>&1; then spinner_stop; error "Ошибка pip:"; tail -10 /tmp/localtaskclaw_pip.log; exit 1 fi if [[ -f "$CODE_DIR/bot/requirements.txt" ]]; then "$VENV_DIR/bin/pip" install -q -r "$CODE_DIR/bot/requirements.txt" >> /tmp/localtaskclaw_pip.log 2>&1 || true fi spinner_stop; success "Зависимости установлены" # Generate API secret API_SECRET=$(generate_secret) # Write secrets/core.env SECRETS_DIR="$CODE_DIR/secrets" mkdir -p "$SECRETS_DIR" chmod 700 "$SECRETS_DIR" cat > "$SECRETS_DIR/core.env" << ENV # LocalTaskClaw Core — generated by installer # DO NOT COMMIT MODEL=${MODEL_NAME} LLM_BASE_URL=${LLM_BASE_URL} LLM_API_KEY=${LLM_API_KEY} BOT_TOKEN=${TG_TOKEN} OWNER_ID=${OWNER_ID} API_SECRET=${API_SECRET} WORKSPACE=${WORKSPACE_DIR} DB_PATH=${DB_PATH} BRAVE_API_KEY=${BRAVE_KEY:-} MAX_ITERATIONS=20 COMMAND_TIMEOUT=60 MAX_TOKENS=4096 API_PORT=11387 ENV chmod 600 "$SECRETS_DIR/core.env" success "Конфиг: ${BOLD}$SECRETS_DIR/core.env${NC}" # Write bot config cat > "$SECRETS_DIR/bot.env" << ENV CORE_URL=http://localhost:11387 BOT_TOKEN=${TG_TOKEN} API_SECRET=${API_SECRET} OWNER_ID=${OWNER_ID} ENV chmod 600 "$SECRETS_DIR/bot.env" # --- macOS: LaunchAgent --- if [[ "$(uname -s)" == "Darwin" ]]; then LAUNCH_DIR="$HOME/Library/LaunchAgents" mkdir -p "$LAUNCH_DIR" cat > "$LAUNCH_DIR/io.localtaskclaw.core.plist" << PLIST Label io.localtaskclaw.core ProgramArguments ${VENV_DIR}/bin/python -muvicorn api:app --host0.0.0.0 --port11387 WorkingDirectory ${CODE_DIR}/core EnvironmentVariables ENV_FILE ${SECRETS_DIR}/core.env StandardOutPath /tmp/localtaskclaw-core.log StandardErrorPath /tmp/localtaskclaw-core.log RunAtLoad KeepAlive PLIST cat > "$LAUNCH_DIR/io.localtaskclaw.bot.plist" << PLIST Label io.localtaskclaw.bot ProgramArguments ${VENV_DIR}/bin/python ${CODE_DIR}/bot/main.py WorkingDirectory ${CODE_DIR}/bot EnvironmentVariables ENV_FILE ${SECRETS_DIR}/bot.env StandardOutPath /tmp/localtaskclaw-bot.log StandardErrorPath /tmp/localtaskclaw-bot.log RunAtLoad KeepAlive PLIST # Load services launchctl unload "$LAUNCH_DIR/io.localtaskclaw.core.plist" 2>/dev/null || true launchctl unload "$LAUNCH_DIR/io.localtaskclaw.bot.plist" 2>/dev/null || true launchctl load "$LAUNCH_DIR/io.localtaskclaw.core.plist" launchctl load "$LAUNCH_DIR/io.localtaskclaw.bot.plist" success "LaunchAgents зарегистрированы (автозапуск при логине)" # --- Linux: systemd user units --- elif command -v systemctl &>/dev/null; then SYSTEMD_DIR="$HOME/.config/systemd/user" mkdir -p "$SYSTEMD_DIR" cat > "$SYSTEMD_DIR/localtaskclaw-core.service" << SVC [Unit] Description=LocalTaskClaw Core After=network.target [Service] Type=simple WorkingDirectory=${CODE_DIR}/core ExecStart=${VENV_DIR}/bin/python -m uvicorn api:app --host 0.0.0.0 --port 11387 Environment=ENV_FILE=${SECRETS_DIR}/core.env Restart=always RestartSec=5 StandardOutput=append:/tmp/localtaskclaw-core.log StandardError=append:/tmp/localtaskclaw-core.log [Install] WantedBy=default.target SVC cat > "$SYSTEMD_DIR/localtaskclaw-bot.service" << SVC [Unit] Description=LocalTaskClaw Bot After=localtaskclaw-core.service Requires=localtaskclaw-core.service [Service] Type=simple WorkingDirectory=${CODE_DIR}/bot ExecStart=${VENV_DIR}/bin/python main.py Environment=ENV_FILE=${SECRETS_DIR}/bot.env Restart=always RestartSec=5 StandardOutput=append:/tmp/localtaskclaw-bot.log StandardError=append:/tmp/localtaskclaw-bot.log [Install] WantedBy=default.target SVC systemctl --user daemon-reload systemctl --user enable --now localtaskclaw-core.service systemctl --user enable --now localtaskclaw-bot.service success "systemd user units запущены" else # Fallback: bare nohup warn "systemd/launchd не найдены — запускаю через nohup" pkill -f "uvicorn api:app" 2>/dev/null || true pkill -f "localtaskclaw-bot" 2>/dev/null || true sleep 1 cd "$CODE_DIR/core" ENV_FILE="$SECRETS_DIR/core.env" \ nohup "$VENV_DIR/bin/python" -m uvicorn api:app --host 0.0.0.0 --port 11387 \ > /tmp/localtaskclaw-core.log 2>&1 & cd "$CODE_DIR/bot" ENV_FILE="$SECRETS_DIR/bot.env" \ nohup "$VENV_DIR/bin/python" main.py \ > /tmp/localtaskclaw-bot.log 2>&1 & success "Процессы запущены (nohup)" fi # Health check spinner_start "Жду готовности сервиса..." HEALTHY=false for i in $(seq 1 20); do if curl -s --max-time 2 http://localhost:11387/health &>/dev/null; then HEALTHY=true; break fi sleep 2 done spinner_stop [[ "$HEALTHY" == "true" ]] && success "Core готов!" || warn "Core ещё загружается (см. /tmp/localtaskclaw-core.log)" ADMIN_URL="http://localhost:11387/admin" MANAGE_INFO="Логи: ${BOLD}ltc logs${NC} Стоп: ${BOLD}ltc stop${NC} Обновить: ${BOLD}ltc update${NC}" fi # ============================================================================= # ФИНАЛ # ============================================================================= echo "" echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════╗${NC}" echo -e "${BOLD}${GREEN}║ Установка завершена! ║${NC}" echo -e "${BOLD}${GREEN}╠══════════════════════════════════════════════════════════╣${NC}" echo -e "${BOLD}${GREEN}║${NC} $(_pad "Режим:" 16) ${BOLD}$(_pad "$MODE_NAME" 36)${NC}${BOLD}${GREEN}║${NC}" echo -e "${BOLD}${GREEN}║${NC} $(_pad "Admin UI:" 16) ${BOLD}$(_pad "$ADMIN_URL" 36)${NC}${BOLD}${GREEN}║${NC}" echo -e "${BOLD}${GREEN}║${NC} $(_pad "API Secret:" 16) ${BOLD}$(_pad "$API_SECRET" 36)${NC}${BOLD}${GREEN}║${NC}" echo -e "${BOLD}${GREEN}║${NC} $(_pad "Telegram бот:" 16) ${BOLD}$(_pad "@$BOT_USERNAME" 36)${NC}${BOLD}${GREEN}║${NC}" echo -e "${BOLD}${GREEN}║${NC} $(_pad "Модель:" 16) ${BOLD}$(_pad "$MODEL_NAME" 36)${NC}${BOLD}${GREEN}║${NC}" echo -e "${BOLD}${GREEN}║${NC} $(_pad "LLM URL:" 16) ${BOLD}$(_pad "${LLM_BASE_URL:0:36}" 36)${NC}${BOLD}${GREEN}║${NC}" if [[ "$MODE_NAME" == "restricted" ]]; then echo -e "${BOLD}${GREEN}║${NC} $(_pad "Workspace:" 16) ${BOLD}$(_pad "~/.localtaskclaw/workspace" 36)${NC}${BOLD}${GREEN}║${NC}" fi echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "$MANAGE_INFO" echo "" info "Открой в браузере: ${BOLD}${ADMIN_URL}${NC}" info "Используй API Secret как пароль для входа в UI" echo "" echo -e "${BOLD}${CYAN} Полезные команды:${NC}" # Install ltc CLI chmod +x "$CODE_DIR/ltc" mkdir -p "$HOME/bin" ln -sf "$CODE_DIR/ltc" "$HOME/bin/ltc" # Add ~/bin to PATH if not already there if ! echo "$PATH" | grep -q "$HOME/bin"; then for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do if [[ -f "$rc" ]] && ! grep -q 'HOME/bin' "$rc"; then echo 'export PATH="$HOME/bin:$PATH"' >> "$rc" fi done export PATH="$HOME/bin:$PATH" fi echo -e " ${GREEN}ltc${NC} CLI installed. Commands:" echo "" echo -e " ${BOLD}ltc status${NC} Check if services are running" echo -e " ${BOLD}ltc stop${NC} Stop services" echo -e " ${BOLD}ltc start${NC} Start services" echo -e " ${BOLD}ltc restart${NC} Restart services" echo -e " ${BOLD}ltc logs${NC} Tail core logs" echo -e " ${BOLD}ltc test${NC} Run tests" echo -e " ${BOLD}ltc seed${NC} Load demo kanban board" echo -e " ${BOLD}ltc update${NC} Update to latest version" echo -e " ${BOLD}ltc open${NC} Open admin UI in browser" echo -e " ${BOLD}ltc uninstall${NC} Remove everything" echo "" echo -e " Admin UI: ${BOLD}${ADMIN_URL}${NC} (password: API Secret above)" echo "" # ── Предложение запустить демо канбан ── echo "" echo -e "${BOLD}${CYAN}── Тестовый запуск ──${NC}" echo "" echo -e " Хочешь загрузить демо-доску с AI-агентами?" echo -e " Будут созданы: 4 воркера + 1 оркестратор + 5 задач." echo -e " Оркестратор автоматически запустит всех агентов." echo "" echo -ne "${BOLD} Запустить демо? [y/N]${NC}: " >/dev/tty read -r RUN_DEMO /dev/tty read -r RUN_ORC /dev/null \ | python3 -c "import sys,json; tasks=json.load(sys.stdin).get('tasks',[]); orc=[t for t in tasks if t.get('agent_role')=='orchestrator']; print(orc[0]['id'] if orc else '')" 2>/dev/null) if [[ -n "$ORC_TASK_ID" ]]; then curl -s -X POST -H "X-Api-Key: ${API_SECRET}" "http://localhost:11387/kanban/tasks/${ORC_TASK_ID}/run" >/dev/null 2>&1 success "Оркестратор запущен (задача #${ORC_TASK_ID})! Следи в UI: ${BOLD}${ADMIN_URL}#kanban${NC}" else warn "Не удалось найти задачу оркестратора. Запусти вручную в UI." fi fi else warn "Ошибка загрузки демо. Можно запустить позже: ${BOLD}python ~/.localtaskclaw/app/scripts/seed_kanban.py${NC}" fi fi echo ""