#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # TaskZilla Installer v2 — Mode 1 (one-click: download + config + provision) # # Mode 2 (OpenClaw-native, tarball you already have): # openclaw plugins install addon-taskzilla.tgz # python3 ~/.openclaw/extensions/addon-taskzilla/scripts/postinstall.py --ci --standard # openclaw gateway restart # (or: bash …/addon-taskzilla/scripts/install-via-openclaw.sh ) # # Mode 3 (engineering checkout → OpenClaw, no tarball): # bash install.sh --local # bash scripts/install.sh --local # bash scripts/install.sh --addon-dir /path/to/taskzilla # (Windows: install.ps1 -Local / -LocalPath → win_plugin_link.py) # # Usage (fully guided): curl -fsSL https://get.taskzilla.ai/install.sh | bash -s -- --setup-code TZ-XXXXXX # Usage (paid license): curl -fsSL https://get.taskzilla.ai/install.sh | bash -s -- --license TZ-XXXX-XXXX-XXXX-XXXX # Usage (Community/free): curl -fsSL https://get.taskzilla.ai/install.sh | bash (auto-mints a TZ-FREE-* starter) # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail # Cloudflare challenges bare curl from some VPS/datacenter IPs (CF 403). # Browser UA for every curl in this process (and children that honor CURL_HOME). _TZ_CURL_UA="${TASKZILLA_CURL_UA:-Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36}" _TZ_CURL_HOME="$(mktemp -d "${TMPDIR:-/tmp}/tz-curl-home.XXXXXX")" export CURL_HOME="${_TZ_CURL_HOME}" printf 'user-agent = "%s"\n' "${_TZ_CURL_UA}" >"${CURL_HOME}/.curlrc" # Cleanup CURL_HOME on exit; later traps must chain (see TGZ trap below). # Always succeed — a failing EXIT trap can become the shell's exit status # under set -e, which made Mode-1 look failed after "TaskZilla is awake". _tz_install_cleanup() { rm -rf "${_TZ_CURL_HOME:-}" 2>/dev/null || true; return 0; } trap '_tz_install_cleanup' EXIT # CDN bootstrap — curl pipe has no sibling install_python.sh/install_voice.sh; fetch or use server dir. # CURL_HOME browser UA is set above (install.sh) so these curls survive Cloudflare bot challenges. UPDATE_BASE="${TASKZILLA_UPDATE_BASE:-https://get.taskzilla.ai}" _SCRIPT_CAND="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)" || true if [[ -n "${_SCRIPT_CAND}" && -f "${_SCRIPT_CAND}/install_python.sh" ]]; then SCRIPT_DIR="${_SCRIPT_CAND}" elif [[ -f "/opt/taskzilla-update/install_python.sh" ]]; then SCRIPT_DIR="/opt/taskzilla-update" else SCRIPT_DIR="${HOME}/.cache/taskzilla-install" mkdir -p "${SCRIPT_DIR}" if ! curl -fsSL "${UPDATE_BASE}/install_python.sh" -o "${SCRIPT_DIR}/install_python.sh"; then echo "❌ Could not download install_python.sh from ${UPDATE_BASE}" >&2 exit 1 fi fi if [[ ! -f "${SCRIPT_DIR}/install_voice.sh" ]]; then if ! curl -fsSL "${UPDATE_BASE}/install_voice.sh" -o "${SCRIPT_DIR}/install_voice.sh"; then echo "❌ Could not download install_voice.sh from ${UPDATE_BASE}" >&2 exit 1 fi fi if [[ ! -f "${SCRIPT_DIR}/verify_addon_tarball.py" ]]; then curl -fsSL "${UPDATE_BASE}/verify_addon_tarball.py" -o "${SCRIPT_DIR}/verify_addon_tarball.py" 2>/dev/null || true fi REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" # shellcheck source=install_python.sh source "${SCRIPT_DIR}/install_python.sh" # shellcheck source=install_voice.sh source "${SCRIPT_DIR}/install_voice.sh" OPENCLAW_DIR="${HOME}/.openclaw" OC_JSON="${OPENCLAW_DIR}/openclaw.json" echo "" banner " 🦖 TaskZilla Installer v2" dim " tiny arms, massive impact" echo "" # ── Parse args ──────────────────────────────────────────────────────────────── SETUP_CODE="" SETUP_PASSWORD="${TASKZILLA_SETUP_PASSWORD:-}" LICENSE_KEY="" SKIP_SYNC="" LOCAL_INSTALL="" while [[ $# -gt 0 ]]; do case "$1" in --setup-code) SETUP_CODE="${2:-}"; shift 2 ;; --setup-password) SETUP_PASSWORD="${2:-}"; shift 2 ;; --license|--license-key) LICENSE_KEY="${2:-}"; shift 2 ;; --skip-sync) SKIP_SYNC=1; shift ;; --local) LOCAL_INSTALL="${REPO_ROOT}" shift ;; --addon-dir) LOCAL_INSTALL="${2:-}" [[ -n "$LOCAL_INSTALL" ]] || err "--addon-dir requires a path" shift 2 ;; *) shift ;; esac done # --skip-sync is an accepted no-op, kept for CLI compat — this no-op # reference keeps the linter from treating it as a dead assignment. : "${SKIP_SYNC}" if [[ -n "$SETUP_CODE" ]]; then [[ "$SETUP_CODE" =~ ^TZ-[A-Z0-9]{6}$ ]] || err "Invalid setup code format (expected TZ-XXXXXX)" fi if [[ -n "$LICENSE_KEY" ]]; then # Accept any of: # TZ-XXXX-XXXX-XXXX-XXXX (canonical paid format, all tiers) # TZ-FREE-XXXX-XXXX-COMC (Community starter — locally minted) # TZ-COM/PER/PRO/ENT-XXXX-XXXX-XXXX (legacy prefixed, back-compat only) # TZ-COM/PER/PRO/ENT- (legacy dev/local-fallback hex) if ! [[ "$LICENSE_KEY" =~ ^TZ-([A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}|FREE-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-COM[0-9A-Za-z]|(COM|PER|PRO|ENT)-([A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}|[0-9a-fA-F]{16,}))$ ]]; then err "Invalid license key format. Community is free — run the installer with NO --license to auto-mint a TZ-FREE-* starter key. Paid keys come from https://taskzilla.ai/pricing" fi fi if [[ -n "$SETUP_CODE" && -n "$LICENSE_KEY" ]]; then err "Pass either --setup-code OR --license, not both." fi # Resolve Python before Community mint (set -u: TZ_PYTHON must exist). # Full dependency check still runs later; this is preflight-only. if ! tz_resolve_python_preflight; then tz_python_fail_message >&2 err "No supported Python (need 3.11–3.13; 3.12 recommended). Python 3.14+ breaks ChromaDB." fi export TASKZILLA_PYTHON="${TZ_PYTHON}" # ── Auto-mint Community starter key when nothing else is provided ─────────── # 2026-05-18: Community is free. If the user runs the installer with no # --setup-code and no --license, we mint a TZ-FREE-{mid8}-COMC key from # machine_id (same algorithm as license.py:community_key) so the tarball # download path has an auth token and the user has a visible key to swap # on upgrade. No server round-trip, no signup form. if [[ -z "$SETUP_CODE" && -z "$LICENSE_KEY" ]]; then LICENSE_KEY=$("$TZ_PYTHON" - << 'MINT_KEY' import hashlib, platform from pathlib import Path raw = f"{platform.node()}|{platform.system()}|{platform.machine()}" for f in ("/etc/machine-id", "/var/lib/dbus/machine-id"): try: raw += Path(f).read_text().strip()[:32]; break except: pass mid = hashlib.sha256(raw.encode()).hexdigest()[:16].upper() mid = (mid + "00000000")[:8] g1, g2, g3 = "FREE", mid[:4], mid[4:8] xor = 0 for c in (g1+g2+g3).upper(): xor ^= ord(c) chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" print(f"TZ-{g1}-{g2}-{g3}-COM{chars[xor % 36]}") MINT_KEY ) if [[ -n "$LICENSE_KEY" ]]; then info "No --license given — minted Community starter: $LICENSE_KEY" info " (Save this key. Upgrade at https://taskzilla.ai/pricing — same key shape, just paste a paid one in env to switch tier.)" fi fi # ── 1. Dependency check ─────────────────────────────────────────────────────── # ADR-024 / releases/latest.json min_openclaw — Mode 1 ensures pin via npm, then # fail-closed if version still mismatches. OPENCLAW_PIN="${TASKZILLA_OPENCLAW_PIN:-2026.6.11}" ensure_openclaw_pin() { # Install/upgrade global OpenClaw to OPENCLAW_PIN when missing or drifted. local need_install=0 local ver="" if ! command -v openclaw >/dev/null 2>&1; then need_install=1 info "OpenClaw not on PATH — installing openclaw@${OPENCLAW_PIN} via npm…" else ver="$(openclaw --version 2>/dev/null | head -n1 || true)" if [[ ! "$ver" =~ ${OPENCLAW_PIN//./\.} ]]; then need_install=1 info "OpenClaw version drift (got: ${ver:-unknown}) — upgrading to openclaw@${OPENCLAW_PIN}…" fi fi if [[ "$need_install" -eq 1 ]]; then command -v npm >/dev/null 2>&1 \ || err "npm not found (need Node 22+). Install Node, then re-run — or: npm install -g openclaw@${OPENCLAW_PIN}" npm install -g "openclaw@${OPENCLAW_PIN}" 2>&1 | sed 's/^/ /' \ || err "npm install -g openclaw@${OPENCLAW_PIN} failed (see https://openclaw.ai/install)" fi ver="$(openclaw --version 2>/dev/null | head -n1 || true)" [[ "$ver" =~ ${OPENCLAW_PIN//./\.} ]] \ || err "OpenClaw version mismatch after ensure (need ${OPENCLAW_PIN}, got: ${ver:-unknown})" ok "OpenClaw" "${ver}" } section "Dependencies" "Making sure the ground is solid before we build on it." for cmd in python3 curl npm; do if ! command -v "$cmd" >/dev/null 2>&1; then if [[ "$cmd" == "npm" ]]; then err "npm not found (need Node 22+ for OpenClaw). Install Node from https://nodejs.org then re-run." else err "'$cmd' not found. Install it with your system package manager (apt / brew / pacman) and re-run." fi fi done ensure_openclaw_pin if [[ ! -f "$OC_JSON" ]]; then info "No OpenClaw home yet — initializing ~/.openclaw…" openclaw onboard --non-interactive --accept-risk --mode local --skip-channels --no-install-daemon --skip-health 2>&1 | sed 's/^/ /' \ || err "OpenClaw init failed after ensure (pin ${OPENCLAW_PIN})" fi [[ -f "$OC_JSON" ]] || err "openclaw.json still missing at ${OC_JSON} after onboard." if ! tz_resolve_python_preflight; then tz_python_fail_message >&2 err "No supported Python (need 3.11–3.13; 3.12 recommended). Python 3.14+ breaks ChromaDB." fi export TASKZILLA_PYTHON="${TZ_PYTHON}" ok "Dependencies present" ok "Python for TaskZilla" "${TZ_PYTHON}" # ── 2. Fetch + apply config ────────────────────────────────────────────────── if [[ -n "$SETUP_CODE" ]]; then section "Configuration" "Pulling your setup down from onboard.taskzilla.ai." info "Fetching configuration for ${SETUP_CODE} …" ONBOARD_APPLY="${SCRIPT_DIR}/onboard_apply.py" if [[ ! -f "$ONBOARD_APPLY" ]]; then ONBOARD_DIR="$(mktemp -d)" ONBOARD_APPLY="${ONBOARD_DIR}/onboard_apply.py" if ! curl -fsSL --connect-timeout 15 "https://get.taskzilla.ai/scripts/onboard_apply.py" -o "$ONBOARD_APPLY" 2>/dev/null; then rm -rf "$ONBOARD_DIR" err "onboard_apply.py not found — update CDN bundle or run from repo checkout (scripts/onboard_apply.py)" fi # ADR-042 + SETUP hardening siblings (import path next to onboard_apply.py) curl -fsSL --connect-timeout 15 "https://get.taskzilla.ai/scripts/channel_day_one.py" \ -o "${ONBOARD_DIR}/channel_day_one.py" 2>/dev/null \ || err "channel_day_one.py missing on CDN — sync_infra_installers + deploy update server" curl -fsSL --connect-timeout 15 "https://get.taskzilla.ai/scripts/onboard_payload_security.py" \ -o "${ONBOARD_DIR}/onboard_payload_security.py" 2>/dev/null \ || err "onboard_payload_security.py missing on CDN — sync_infra_installers + deploy update server" fi ONBOARD_ARGS=(--setup-code "$SETUP_CODE") if [[ -n "$SETUP_PASSWORD" ]]; then ONBOARD_ARGS+=(--setup-password "$SETUP_PASSWORD") fi "$TZ_PYTHON" "$ONBOARD_APPLY" "${ONBOARD_ARGS[@]}" 2>&1 | sed 's/^/ /' \ || err "Setup code invalid or already used — visit https://onboard.taskzilla.ai" ok "Configuration applied" if [[ -z "${LICENSE_KEY:-}" ]]; then LICENSE_KEY=$("$TZ_PYTHON" -c "import json; from pathlib import Path; c=json.loads((Path.home()/'.openclaw'/'openclaw.json').read_text()); print(c.get('env',{}).get('TASKZILLA_LICENSE_KEY','') or '')" 2>/dev/null || true) fi elif [[ -z "$LICENSE_KEY" ]]; then warn "No --setup-code — Telegram/ClickUp not configured yet." info "For a working bot: https://onboard.taskzilla.ai → re-run with --setup-code TZ-XXXXXX" fi # ── 2e. Inject --license key into openclaw.json (community fast-path) ──────── # When the user passes --license directly (no setup code), still wire the # license key into env so postinstall.py picks it up and stamps the tier. if [[ -n "$LICENSE_KEY" && -z "$SETUP_CODE" ]]; then info "Stamping license key into openclaw.json…" "$TZ_PYTHON" - << APPLY_LICENSE import json, os, shutil from datetime import datetime cfg_path = os.path.expanduser("~/.openclaw/openclaw.json") shutil.copy2(cfg_path, cfg_path + f".bak.{datetime.now().strftime('%Y%m%d%H%M%S')}") with open(cfg_path) as f: cfg = json.load(f) cfg.setdefault("env", {})["TASKZILLA_LICENSE_KEY"] = "$LICENSE_KEY" with open(cfg_path, "w") as f: json.dump(cfg, f, indent=2, ensure_ascii=False); f.write("\n") print(" ✓ TASKZILLA_LICENSE_KEY") APPLY_LICENSE ok "License key applied" fi # ── 3–4. Download + install addon (or Mode 3 --local engineering checkout) ─── ADDON_DIR="${HOME}/.openclaw/extensions/addon-taskzilla" if [[ -n "$LOCAL_INSTALL" ]]; then # Mode 3 — place from git checkout (no tarball). Prefer OpenClaw-native # `plugins install --link` via win_plugin_link.py (same helper as Windows # -Local); fall back to symlink/copy if the helper is missing. section "Addon" "Mode 3 — engineering checkout (link, no tarball)." [[ -f "${LOCAL_INSTALL}/openclaw.plugin.json" ]] || err "--local path is not an addon root: ${LOCAL_INSTALL}" LOCAL_INSTALL="$(cd "$LOCAL_INSTALL" && pwd)" export TASKZILLA_ADDON_DIR="$LOCAL_INSTALL" info "Mode 3 local install from ${LOCAL_INSTALL} (no download)…" LINK_HELPER="${LOCAL_INSTALL}/scripts/win_plugin_link.py" _mode3_linked=0 if [[ -f "$LINK_HELPER" ]]; then if "$TZ_PYTHON" "$LINK_HELPER" "$LOCAL_INSTALL"; then _mode3_linked=1 ok "Linked local addon (plugins install --link / plugins.load.paths)" else warn "win_plugin_link.py failed — falling back to symlink/copy" fi else warn "win_plugin_link.py missing — falling back to symlink/copy" fi if [[ "${_mode3_linked}" -eq 0 ]]; then if [[ "$LOCAL_INSTALL" != "$ADDON_DIR" ]]; then mkdir -p "$(dirname "$ADDON_DIR")" if [[ -L "$ADDON_DIR" || -d "$ADDON_DIR" ]]; then rm -rf "$ADDON_DIR" fi ln -sfn "$LOCAL_INSTALL" "$ADDON_DIR" 2>/dev/null || cp -a "$LOCAL_INSTALL" "$ADDON_DIR" fi ok "Using local addon (symlink/copy fallback)" fi # Resolve skills/postinstall from the checkout (Windows -Local parity). ADDON_DIR="$LOCAL_INSTALL" export TASKZILLA_ADDON_DIR="$LOCAL_INSTALL" else section "Addon" "Fetching TaskZilla and wiring it into OpenClaw." # Tarball download requires a license — community keys (TZ-COM-...) work too. DOWNLOAD_AUTH="" if [[ -n "$LICENSE_KEY" ]]; then DOWNLOAD_AUTH="$LICENSE_KEY" elif [[ -n "$SETUP_CODE" ]]; then DOWNLOAD_AUTH="$SETUP_CODE" fi if [[ -z "$DOWNLOAD_AUTH" ]]; then err "Cannot download addon — local Community starter mint failed. Check python3 is installed, or pass --license TZ-XXXX-... explicitly." fi info "Authenticating with the update server…" TOKEN_RESPONSE=$(curl -fsS -X POST "https://get.taskzilla.ai/api/auth/token" \ -H "Content-Type: application/json" \ -d "{\"license\":\"$DOWNLOAD_AUTH\"}" 2>&1) \ || err "Token exchange failed. Is the license key valid? ($TOKEN_RESPONSE)" DL_TOKEN=$(echo "$TOKEN_RESPONSE" | "$TZ_PYTHON" -c "import sys,json; print(json.loads(sys.stdin.read()).get('token',''))") [[ -n "$DL_TOKEN" ]] || err "Update server returned no token: $TOKEN_RESPONSE" ok "Authenticated" TGZ="$(mktemp "${TMPDIR:-/tmp}/addon-taskzilla.XXXXXX.tgz")" trap 'rm -f "$TGZ"; _tz_install_cleanup' EXIT info "Downloading TaskZilla…" # Retry on Cloudflare / update-server 429–503 (fresh Mode-1 after wipe can hit rate limits). _dl_ok=0 for _dl_try in 1 2 3 4 5; do if curl -fsSL --retry 2 --retry-delay 2 \ "https://get.taskzilla.ai/download/addon-taskzilla.tgz" \ -H "Authorization: Bearer $DL_TOKEN" \ -o "$TGZ"; then _dl_ok=1 break fi warn "Download attempt ${_dl_try}/5 failed — waiting $((_dl_try * 5))s…" sleep "$((_dl_try * 5))" # Refresh short-lived download token between attempts TOKEN_RESPONSE=$(curl -fsS -X POST "https://get.taskzilla.ai/api/auth/token" \ -H "Content-Type: application/json" \ -d "{\"license\":\"$DOWNLOAD_AUTH\"}" 2>&1) || true DL_TOKEN=$(echo "$TOKEN_RESPONSE" | "$TZ_PYTHON" -c "import sys,json; print(json.loads(sys.stdin.read()).get('token',''))" 2>/dev/null || true) done [[ "$_dl_ok" -eq 1 ]] || err "Download failed after retries (429/network). Wait a minute and re-run the same one-liner." ok "Addon downloaded" info "Verifying tarball SHA-256 against latest.json…" VERIFY_TGZ="${SCRIPT_DIR}/verify_addon_tarball.py" if [[ -f "$VERIFY_TGZ" ]]; then "$TZ_PYTHON" "$VERIFY_TGZ" "$TGZ" \ || err "Tarball integrity check failed — refusing to install. Re-download from a trusted network." else # curl|bash CDN path: installer has no sibling scripts/ — inline verify. "$TZ_PYTHON" - "$TGZ" <<'PY' \ || err "Tarball integrity check failed — refusing to install. Re-download from a trusted network." import hashlib, json, sys, urllib.request path = sys.argv[1] h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) digest = h.hexdigest() req = urllib.request.Request( # nosec B310 "https://get.taskzilla.ai/latest.json", headers={"User-Agent": "TaskZilla-install/1.0", "Accept": "application/json"}, ) with urllib.request.urlopen(req, timeout=15) as r: # nosec B310 meta = json.loads(r.read().decode()) expect = (meta.get("sha256") or meta.get("checksum") or "").strip().lower() if not expect: print("latest.json missing sha256/checksum", file=sys.stderr) raise SystemExit(1) if digest != expect: print(f"SHA-256 mismatch: got {digest[:16]}… expected {expect[:16]}…", file=sys.stderr) raise SystemExit(1) print(f"sha256={digest[:16]}… ok") PY fi ok "Tarball integrity verified" info "Installing addon…" # Fresh Mode-1: clear stub/partial addon dirs so plugins install cannot no-op. if [[ -e "$ADDON_DIR" ]]; then rm -rf "$ADDON_DIR" fi openclaw plugins install "$TGZ" --force \ || err "Plugin install failed. Run 'openclaw logs' for details." [[ -f "${ADDON_DIR}/openclaw.plugin.json" ]] \ || err "Plugin install reported OK but openclaw.plugin.json missing at ${ADDON_DIR}" ok "Addon installed" rm -f "$TGZ" # Restore CURL_HOME cleanup (do not leave EXIT trap empty after TGZ cleanup). trap '_tz_install_cleanup' EXIT fi # ── 4b. Provision (crons, timers, seeds, daemons) ──────────────────────────── # `openclaw plugins install` only places files + loads the plugin. It does NOT # install the 73 cron templates, 9 systemd timers, knowledge API service, ollama # models, memory/belief seeds, or constellation daemon — that's postinstall.py. # Without this step, fresh installs load the plugin but never schedule anything. section "Provisioning" "Crons, timers, services, seeds — the machinery under the machinery." if ! tz_resolve_python_from_addon "$ADDON_DIR"; then tz_python_fail_message >&2 err "Could not resolve Python after addon install." fi export TASKZILLA_PYTHON="${TZ_PYTHON}" ok "Python pinned for skills" "${TZ_PYTHON}" POSTINSTALL="${ADDON_DIR}/scripts/postinstall.py" if [[ -f "$POSTINSTALL" ]]; then info "Handing off to postinstall.py — 46 steps, mostly quick…" # Mode: read from openclaw.json (install.sh stamps cfg.taskzilla.mode above # when --setup-code is used). Default to 'standard' otherwise. TZ_MODE=$("$TZ_PYTHON" -c " import json, os try: cfg = json.load(open(os.path.expanduser('~/.openclaw/openclaw.json'))) print(cfg.get('taskzilla', {}).get('mode', 'standard')) except Exception: print('standard') ") case "$TZ_MODE" in fleet) POSTINSTALL_MODE="--fleet" ;; *) POSTINSTALL_MODE="--standard" ;; esac POSTINSTALL_OK=0 # Pipefail would otherwise take sed's status; only postinstall's rc matters. set +e "$TZ_PYTHON" "$POSTINSTALL" --ci "$POSTINSTALL_MODE" --skip-completed 2>&1 | sed 's/^/ /' _post_rc="${PIPESTATUS[0]}" set -e if [[ "${_post_rc}" -eq 0 ]]; then POSTINSTALL_OK=1 tz_apply_openclaw_python_bin "$ADDON_DIR" if tz_install_skip_post_postinstall; then info "Postinstall finalized + gateway restarted — skipping duplicate layout/defaults/restart" else COMPAT="${ADDON_DIR}/scripts/openclaw_config_compat.py" if [[ -f "$COMPAT" ]]; then info "OpenClaw layout repair (no double .openclaw)…" "$TZ_PYTHON" "$COMPAT" 2>&1 | sed 's/^/ /' || true fi DEFAULTS="${ADDON_DIR}/scripts/install_plugin_defaults.py" if [[ -f "$DEFAULTS" ]]; then info "Applying full plugin + hook defaults…" "$TZ_PYTHON" "$DEFAULTS" --openclaw-home "$OPENCLAW_DIR" --python-bin "$TZ_PYTHON" --hooks-cli \ 2>&1 | sed 's/^/ /' || true fi fi VERIFY="${ADDON_DIR}/scripts/install_verify.py" if [[ -f "$VERIFY" ]]; then info "Running install verify…" "$TZ_PYTHON" "$VERIFY" --no-agent 2>&1 | sed 's/^/ /' || warn "install verify had warnings" fi ok "Provisioning complete" else err "Provisioning failed — addon files are installed but setup is incomplete. Re-run: $TZ_PYTHON $POSTINSTALL --repair" fi else warn "postinstall.py not found at $POSTINSTALL — skipping provisioning." warn "Plugin is loaded but crons/timers/seeds were NOT installed." fi # ── 5. Restart gateway (single owner — skip if postinstall already restarted) ─ if [[ "${POSTINSTALL_OK:-0}" -eq 1 ]] && tz_install_skip_post_postinstall; then info "Gateway restart skipped (postinstall already handled it)" else info "Restarting the gateway…" openclaw gateway restart \ || err "Gateway restart failed. Try: openclaw gateway restart" ok "Gateway restarted" fi if "$TZ_PYTHON" -c "import json,os; c=json.load(open(os.path.expanduser('~/.openclaw/openclaw.json'))); e=c.get('env',{}); exit(0 if (e.get('TASKZILLA_GROUP_CHAT_ID') or e.get('TELEGRAM_CHAT_ID')) else 1)" 2>/dev/null; then NEXT_STEP="Next: open Telegram and send a message to your group." else NEXT_STEP="Next: open Telegram and DM your bot (DM-only install — no group configured)." fi done_banner "TaskZilla is awake" \ "$NEXT_STEP" \ "Try ${BOLD}/standup${NC} to kick off your first daily standup." \ "" \ "${DIM}Workspace config: ~/.openclaw/workspace-taskzilla/workspace.json${NC}" \ "${DIM}Re-sync anytime: ${TZ_PYTHON} bin/sync_workspace.py${NC}" \ "${DIM}Reconfigure: https://onboard.taskzilla.ai${NC}" # Drop EXIT trap before leaving so cleanup cannot override this status. # Marker: TZ_INSTALL_EXIT_V2 (grep install logs / CDN to confirm fresh script). trap - EXIT _tz_install_cleanup echo "TZ_INSTALL_EXIT_V2" >&2 exit 0