← Files Revelata deepKPIARCHIVED FILE

install.sh

13 KB · Nov 3, 2026 · 18:55 UTC

↓ Download file

#!/usr/bin/env bash
# Unified installer for deepkpi-agents — builds the single-skill ZIP
# (revelata-deepkpi) from the plugin layout under skills/. Supports Claude
# Desktop, Claude.ai (web), and OpenClaw.
#
# Usage:
#   ./install.sh              Interactive — prompts for platform
#   ./install.sh openclaw     Skip prompt, go straight to OpenClaw install
#   curl ... | bash -s openclaw   Fetch from GitHub and install for OpenClaw
set -euo pipefail

GITHUB_CODELOAD="https://codeload.github.com/revelata/deepkpi-agents/tar.gz/main"
# Stable prebuilt Claude zip (built by Makefile release target).
GITHUB_LATEST_ZIP_URL="https://github.com/revelata/deepkpi-agents/releases/latest/download/deepkpi-skills.zip"

# Installed folder name (OpenClaw skills dir and Claude zip root).
# Must match `name:` in installer/orchestrator-template.md.
BUNDLE_DIR_NAME="revelata-deepkpi"
OPENCLAW_SKILL_ENTRY_KEY="${BUNDLE_DIR_NAME}"

OPENCLAW_SKILLS_ROOT="${OPENCLAW_SKILLS_ROOT:-${HOME}/.openclaw/skills}"
CONFIG_FILE="${HOME}/.openclaw/openclaw.json"

die() { echo "Error: $*" >&2; exit 1; }

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || echo "")"
if [[ -z "$SCRIPT_DIR" ]]; then
  die "could not resolve script directory"
fi

# Local clone is detected by the plugin manifest (post-refactor layout).
if [[ -f "${SCRIPT_DIR}/.claude-plugin/plugin.json" ]]; then
  HAS_LOCAL=true
  DEFAULT_REPO_ROOT="$SCRIPT_DIR"
else
  HAS_LOCAL=false
  DEFAULT_REPO_ROOT=""
fi

TMP_EXTRACT=""
TMP_BUNDLE=""
cleanup() {
  if [[ -n "${TMP_EXTRACT}" && -d "${TMP_EXTRACT}" ]]; then
    rm -rf "${TMP_EXTRACT}"
  fi
  if [[ -n "${TMP_BUNDLE}" && -d "${TMP_BUNDLE}" ]]; then
    rm -rf "${TMP_BUNDLE}"
  fi
  return 0
}
trap cleanup EXIT

# ── Helpers ──────────────────────────────────────────────────────────────────

default_zip_out_dir() {
  if [[ -n "${DEEPKPI_ZIP_OUT_DIR:-}" ]]; then
    echo "${DEEPKPI_ZIP_OUT_DIR}"
  elif [[ -d "${HOME}/Desktop" ]]; then
    echo "${HOME}/Desktop/deepkpi-skills"
  elif [[ -d "${HOME}/Downloads" ]]; then
    echo "${HOME}/Downloads/deepkpi-skills"
  else
    echo "${HOME}/deepkpi-skills"
  fi
}

# Allocate TMP_BUNDLE / TMP_EXTRACT in the parent shell so the EXIT trap can
# clean them up. Must be called before any helper that captures stdout via
# $(...) — assignments inside subshells don't propagate to the parent.
allocate_temp_dirs() {
  if [[ -z "${TMP_BUNDLE}" ]]; then
    TMP_BUNDLE="$(mktemp -d)"
  fi
  if ! $HAS_LOCAL && [[ -z "${TMP_EXTRACT}" ]]; then
    TMP_EXTRACT="$(mktemp -d)"
  fi
}

# Download and extract GitHub tarball into pre-allocated TMP_EXTRACT; print
# absolute repo root path on stdout.
fetch_repo_root_from_github() {
  [[ -n "${TMP_EXTRACT}" && -d "${TMP_EXTRACT}" ]] || die "TMP_EXTRACT not allocated"
  echo "Downloading repository tarball from GitHub..." >&2
  (
    cd "$TMP_EXTRACT"
    curl -fsSL "$GITHUB_CODELOAD" | tar -xz
  )
  shopt -s nullglob
  local topdirs=( "$TMP_EXTRACT"/* )
  shopt -u nullglob
  if [[ ${#topdirs[@]} -ne 1 || ! -d "${topdirs[0]}" ]]; then
    die "unexpected archive layout (expected exactly one top-level directory)"
  fi
  echo "${topdirs[0]}"
}

# Resolve directory containing the plugin manifest and skills/ tree.
resolve_repo_root() {
  if $HAS_LOCAL; then
    echo "$DEFAULT_REPO_ROOT"
    return
  fi
  fetch_repo_root_from_github
}

# Lint check: cross-skill references must go through _common/, not directly to
# another skill's SKILL.md. The latter works in plugin context but breaks in
# the ZIP because skill SKILL.md files are renamed to <name>/<name>.md.
lint_skills() {
  local repo_root="$1"
  local skills_root="${repo_root}/skills"
  local hits
  hits="$(grep -rEn '\.\./[^_][^/]*/SKILL\.md' "$skills_root" 2>/dev/null \
    | grep -Ev 'skills/operating-model/' || true)"
  if [[ -n "$hits" ]]; then
    echo "Cross-skill SKILL.md references found (must go through _common/):" >&2
    echo "$hits" >&2
    die "lint failed"
  fi
}

# Build ${dest_parent}/${BUNDLE_DIR_NAME} from repo root in plugin layout.
build_bundle_at() {
  local repo_root="$1"
  local dest_parent="$2"
  local bundle_root="${dest_parent}/${BUNDLE_DIR_NAME}"

  local skills_root="${repo_root}/skills"
  local common_root="${skills_root}/_common"
  local template="${repo_root}/installer/orchestrator-template.md"

  [[ -d "$skills_root" ]] || die "skills/ not found in ${repo_root}"
  [[ -d "$common_root" ]] || die "skills/_common/ not found"
  [[ -f "$common_root/deepkpi-api.md" ]] || die "skills/_common/deepkpi-api.md not found"
  [[ -f "$template" ]] || die "installer/orchestrator-template.md not found"

  lint_skills "$repo_root"

  rm -rf "$bundle_root"
  mkdir -p "$bundle_root"

  # 1. Generated root SKILL.md (registered orchestrator for the ZIP).
  cp "$template" "${bundle_root}/SKILL.md"

  # 2. Each skill: skills/<name>/SKILL.md  →  <bundle>/<name>/<name>.md
  #    Plus references/ subfolder if present. Other subfolders (evals/) are
  #    development-only and not bundled.
  local skill_dir skill_name
  shopt -s nullglob
  for skill_dir in "${skills_root}"/*/; do
    skill_name="$(basename "$skill_dir")"
    [[ "$skill_name" == "_common" ]] && continue
    [[ "$skill_name" == "revelata" ]] && continue  # plugin-only orchestrator

    [[ -f "${skill_dir}/SKILL.md" ]] || die "missing SKILL.md in skills/${skill_name}/"

    mkdir -p "${bundle_root}/${skill_name}"
    cp "${skill_dir}/SKILL.md" "${bundle_root}/${skill_name}/${skill_name}.md"
    cp "${skill_dir}/SKILL.md" "${bundle_root}/${skill_name}/SKILL.md"
    if [[ "$skill_name" == "operating-model" ]]; then
      shopt -s nullglob
      for sub in "${skill_dir}"/*/; do
        sub_name="$(basename "${sub%/}")"
        [[ "$sub_name" == "_common" || "$sub_name" == "implied-metric" ]] && continue
        cp -R "${sub%/}" "${bundle_root}/${skill_name}/"
      done
      shopt -u nullglob
    elif [[ -d "${skill_dir}/references" ]]; then
      cp -R "${skill_dir}/references" "${bundle_root}/${skill_name}/references"
    fi
  done
  shopt -u nullglob

  # 3. Shared reference content (includes deepkpi-api.md for non-MCP runtimes).
  cp -R "$common_root" "${bundle_root}/_common"
}

# Stage bundle in pre-allocated TMP_BUNDLE; echo absolute path to .../revelata-deepkpi
stage_bundle() {
  [[ -n "${TMP_BUNDLE}" && -d "${TMP_BUNDLE}" ]] || die "TMP_BUNDLE not allocated"
  local repo_root
  repo_root="$(resolve_repo_root)"
  build_bundle_at "$repo_root" "$TMP_BUNDLE"
  echo "${TMP_BUNDLE}/${BUNDLE_DIR_NAME}"
}

zip_bundle_for_claude() {
  local bundle_path="$1"
  local out_dir="$2"
  local parent
  parent="$(dirname "$bundle_path")"
  [[ "$(basename "$bundle_path")" == "$BUNDLE_DIR_NAME" ]] || die "zip_bundle_for_claude: wrong bundle path"

  mkdir -p "$out_dir"
  rm -f "${out_dir}/${BUNDLE_DIR_NAME}.zip"
  echo "Writing: ${out_dir}/${BUNDLE_DIR_NAME}.zip"
  (cd "$parent" && zip -qr "${out_dir}/${BUNDLE_DIR_NAME}.zip" "$BUNDLE_DIR_NAME")
}

download_latest_zip_for_claude() {
  local out_dir="$1"
  mkdir -p "$out_dir"
  rm -f "${out_dir}/${BUNDLE_DIR_NAME}.zip"
  echo "Downloading latest prebuilt ZIP..."
  echo "  ${GITHUB_LATEST_ZIP_URL}"
  echo "Writing: ${out_dir}/${BUNDLE_DIR_NAME}.zip"
  curl -fsSL -L "${GITHUB_LATEST_ZIP_URL}" -o "${out_dir}/${BUNDLE_DIR_NAME}.zip"
}

install_openclaw_bundle() {
  local bundle_path="$1"
  mkdir -p "$OPENCLAW_SKILLS_ROOT"
  echo "Installing bundled skill into: ${OPENCLAW_SKILLS_ROOT}/${BUNDLE_DIR_NAME}"
  rm -rf "${OPENCLAW_SKILLS_ROOT}/${BUNDLE_DIR_NAME}"
  cp -R "$bundle_path" "${OPENCLAW_SKILLS_ROOT}/${BUNDLE_DIR_NAME}"
  echo "  ✓ ${BUNDLE_DIR_NAME}"
  echo ""
}

configure_openclaw() {
  echo "─── OpenClaw configuration ───"
  echo ""
  printf 'Need an Account? \e]8;;https://www.revelata.com/signup?product_id=0\ahttps://www.revelata.com/signup?product_id=0\e]8;;\a\n'
  printf 'Have an Account? Retrieve your API key here: \e]8;;https://www.revelata.com/ai-credits\ahttps://www.revelata.com/ai-credits\e]8;;\a\n\n'
  read -r -p "Paste your deepKPI API key (dk_...): " DEEPKPI_API_KEY </dev/tty
  if [[ -z "${DEEPKPI_API_KEY}" ]]; then
    echo "No API key provided, aborting."
    exit 1
  fi

  mkdir -p "$(dirname "${CONFIG_FILE}")"

  local entry_key="$OPENCLAW_SKILL_ENTRY_KEY"

  if [[ ! -f "${CONFIG_FILE}" ]]; then
    cat > "${CONFIG_FILE}" <<CFGEOF
{
  "skills": {
    "entries": {
      "${entry_key}": {
        "enabled": true,
        "env": {
          "DEEPKPI_API_KEY": "${DEEPKPI_API_KEY}"
        }
      }
    },
    "load": {
      "extraDirs": ["${OPENCLAW_SKILLS_ROOT}"],
      "watch": true
    }
  }
}
CFGEOF
    echo "Created ${CONFIG_FILE}."
  else
    python3 - <<PYEOF
import json
import pathlib

cfg_path = pathlib.Path("${CONFIG_FILE}")
data = json.loads(cfg_path.read_text())

skills = data.setdefault("skills", {})
entries = skills.setdefault("entries", {})

# Migrate env from legacy keys (old skill ids / per-folder install).
entry = entries.setdefault("${entry_key}", {})
for legacy_key in ("deepkpi-api", "deepkpi"):
    legacy = entries.pop(legacy_key, None)
    if isinstance(legacy, dict):
        entry.setdefault("enabled", legacy.get("enabled", True))
        old_env = legacy.get("env") or {}
        new_env = entry.setdefault("env", {})
        for k, v in old_env.items():
            new_env.setdefault(k, v)
entry["enabled"] = True
env = entry.setdefault("env", {})
env["DEEPKPI_API_KEY"] = "${DEEPKPI_API_KEY}"

load = skills.setdefault("load", {})
extra = load.setdefault("extraDirs", [])
skills_dir = "${OPENCLAW_SKILLS_ROOT}"
if skills_dir not in extra:
    extra.append(skills_dir)
load.setdefault("watch", True)

cfg_path.write_text(json.dumps(data, indent=2))
PYEOF
    echo "Updated ${CONFIG_FILE}."
  fi

  echo ""
  read -r -p "Restart the OpenClaw gateway now? [y/N] " RESTART_CHOICE </dev/tty
  if [[ "${RESTART_CHOICE}" =~ ^[Yy]$ ]]; then
    if command -v openclaw >/dev/null 2>&1; then
      echo "Restarting OpenClaw gateway..."
      openclaw gateway restart || {
        echo "Failed to restart. Please restart OpenClaw manually."
        exit 1
      }
      echo "Gateway restarted."
    else
      echo "'openclaw' CLI not found. Please restart the gateway manually."
    fi
  else
    echo "Skipping restart. Restart OpenClaw manually to pick up the new skills."
  fi
}

# ── Main ─────────────────────────────────────────────────────────────────────

MODE="${1:-}"

if [[ -z "$MODE" ]]; then
  echo ""
  echo "deepkpi-agents skill installer"
  echo ""
  echo "Where will you use this skill?"
  echo "  1) Claude Desktop"
  echo "  2) Claude.ai (web)"
  echo "  3) OpenClaw"
  echo ""
  read -r -p "Enter 1, 2, or 3: " choice </dev/tty || true
  case "${choice:-}" in
    1) MODE="claude-desktop" ;;
    2) MODE="claude-web" ;;
    3) MODE="openclaw" ;;
    *) die "invalid choice (expected 1, 2, or 3)" ;;
  esac
fi

case "$MODE" in
  claude-desktop)
    OUT="$(default_zip_out_dir)"
    if $HAS_LOCAL; then
      allocate_temp_dirs
      BUNDLE_PATH="$(stage_bundle)"
      zip_bundle_for_claude "$BUNDLE_PATH" "$OUT"
    else
      download_latest_zip_for_claude "$OUT"
    fi
    echo ""
    echo "Next steps (Claude Desktop):"
    echo "  1. Open Claude Desktop and sign in."
    echo '  2. Settings → Capabilities → enable "Code execution and file creation".'
    echo "  3. Customize → Skills → + → Upload a skill."
    echo "  4. Upload this single ZIP:"
    echo "       ${OUT}/${BUNDLE_DIR_NAME}.zip"
    echo "  5. Enable the skill with its toggle."
    echo ""
    echo "Packaging rules: https://support.claude.com/en/articles/12512198-creating-custom-skills"
    ;;

  claude-web)
    OUT="$(default_zip_out_dir)"
    if $HAS_LOCAL; then
      allocate_temp_dirs
      BUNDLE_PATH="$(stage_bundle)"
      zip_bundle_for_claude "$BUNDLE_PATH" "$OUT"
    else
      download_latest_zip_for_claude "$OUT"
    fi
    echo ""
    echo "Next steps (Claude.ai):"
    echo "  1. Sign in at https://claude.ai"
    echo '  2. Settings → Capabilities → enable "Code execution and file creation".'
    echo "  3. Open Customize → Skills: https://claude.ai/customize/skills"
    echo "  4. Click + → Upload a skill, and upload:"
    echo "       ${OUT}/${BUNDLE_DIR_NAME}.zip"
    echo "  5. Enable the skill with its toggle."
    echo ""
    echo "Help: https://support.claude.com/en/articles/12512180-using-skills-in-claude"
    ;;

  openclaw)
    echo ""
    echo "deepkpi-agents — OpenClaw installer"
    echo ""
    allocate_temp_dirs
    BUNDLE_PATH="$(stage_bundle)"
    install_openclaw_bundle "$BUNDLE_PATH"
    configure_openclaw
    echo ""
    echo "Installed bundled skill:"
    echo "  • ${BUNDLE_DIR_NAME}/"
    echo ""
    echo "Verify: openclaw skills list"
    echo "Docs:   https://github.com/revelata/deepkpi-agents"
    ;;

  *)
    die "unknown mode: ${MODE} (expected: openclaw, claude-desktop, claude-web, or run without arguments for interactive)"
    ;;
esac

echo ""
echo "Done."

SHA-256: 12398f75dc302e573d3abcac5685659f985f58b30ab786222698f3779ec352ba