Skill: vidarbrekke/ollama-memory-embeddings (version 1.0.4)

--- slug: ollama-memory-embeddings version: "1.0.4" display_name: Ollama Memory Embeddings displayName: Ollama Memory Embeddings name: ollama-memory-embeddings description: > Configure OpenClaw memory search to use Ollama as the embeddings server (OpenAI-compatible /v1/embeddings) instead of the built-in node-llama-cpp local GGUF loading. Includes interactive model selection and optional import of an existing local embedding GGUF into Ollama. --- # Ollama Memory Embeddings This skill configures OpenClaw memory search to use Ollama as the **embeddings server** via its OpenAI-compatible `/v1/embeddings` endpoint. > **Embeddings only.** This skill does not affect chat/completions routing — > it only changes how memory-search embedding vectors are generated. ## What it does - Installs this skill under `~/.openclaw/skills/ollama-memory-embeddings` - Verifies Ollama is installed and reachable - Lets the user choose an embedding model: - `embeddinggemma` (default — closest to OpenClaw built-in) - `nomic-embed-text` (strong quality, efficient) - `all-minilm` (smallest/fastest) - `mxbai-embed-large` (highest quality, larger) - Optionally imports an existing local embedding GGUF into Ollama via `ollama create` (currently detects embeddinggemma, nomic-embed, all-minilm, and mxbai-embed GGUFs in known cache directories) - Normalizes model names (handles `:latest` tag automatically) - Updates `agents.defaults.memorySearch` in OpenClaw config (surgical — only touches keys this skill owns): - `provider = "openai"` - `model = <selected model>:latest` - `remote.baseUrl = "http://127.0.0.1:11434/v1/"` - `remote.[REDACTED_SECRET]"` (required by client, ignored by Ollama) - Performs a post-write config sanity check (reads back and validates JSON) - Optionally restarts the OpenClaw gateway (with detection of available restart methods: `openclaw gateway restart`, systemd, launchd) - Optional memory reindex during install (`openclaw memory index --force --verbose`) - Runs a two-step verification: 1. Checks model exists in `ollama list` 2. Calls the embeddings endpoint and validates the response - Adds an idempotent drift-enforcement command (`enforce.sh`) - Adds optional config drift auto-healing watchdog (`watchdog.sh`) ## Install ```bash bash ~/.openclaw/skills/ollama-memory-embeddings/install.sh ``` From this repository: ```bash bash skills/ollama-memory-embeddings/install.sh ``` ## Non-interactive usage ```bash bash ~/.openclaw/skills/ollama-memory-embeddings/install.sh \ --non-interactive \ --model embeddinggemma \ --reindex-memory auto ``` Bulletproof setup (install watchdog): ```bash bash ~/.openclaw/skills/ollama-memory-embeddings/install.sh \ --non-interactive \ --model embeddinggemma \ --reindex-memory auto \ --install-watchdog \ --watchdog-interval 60 ``` > **Note:** In non-interactive mode, `--import-local-gguf auto` is treated as > `no` (safe default). Use `--import-local-gguf yes` to explicitly opt in. Options: - `--model <id>`: one of `embeddinggemma`, `nomic-embed-text`, `all-minilm`, `mxbai-embed-large` - `--import-local-gguf <auto|yes|no>`: default `no` (safer default; opt in with `yes`) - `--import-model-name <name>`: default `embeddinggemma-local` - `--restart-gateway <yes|no>`: default `no` (restart only when explicitly requested) - `--skip-restart`: deprecated alias for `--restart-gateway no` - `--openclaw-config <path>`: config file path override - `--install-watchdog`: install launchd drift auto-heal watchdog (macOS) - `--watchdog-interval <sec>`: watchdog interval (default 60) - `--reindex-memory <auto|yes|no>`: memory rebuild mode (default `auto`) - `--dry-run`: print planned changes and commands; make no modifications ## Verify ```bash ~/.openclaw/skills/ollama-memory-embeddings/verify.sh ``` Use `--verbose` to dump raw API response on failure: ```bash ~/.openclaw/skills/ollama-memory-embeddings/verify.sh --verbose ``` ## Drift enforcement and auto-heal Manually enforce desired state (safe to run repeatedly): ```bash ~/.openclaw/skills/ollama-memory-embeddings/enforce.sh \ --model embeddinggemma \ --openclaw-config ~/.openclaw/openclaw.json ``` Check for drift only: ```bash ~/.openclaw/skills/ollama-memory-embeddings/enforce.sh \ --check-only \ --model embeddinggemma ``` Run watchdog once (check + heal): ```bash ~/.openclaw/skills/ollama-memory-embeddings/watchdog.sh \ --once \ --model embeddinggemma ``` Install watchdog via launchd (macOS): ```bash ~/.openclaw/skills/ollama-memory-embeddings/watchdog.sh \ --install-launchd \ --model embeddinggemma \ --interval-sec 60 ``` ## GGUF detection scope The installer searches for embedding GGUFs matching these patterns in known cache directories (`~/.node-llama-cpp/models`, `~/.cache/node-llama-cpp/models`, `~/.cache/openclaw/models`): - `*embeddinggemma*.gguf` - `*nomic-embed*.gguf` - `*all-minilm*.gguf` - `*mxbai-embed*.gguf` Other embedding GGUFs are not auto-detected. You can always import manually: ```bash ollama create my-model -f /path/to/Modelfile ``` ## Notes - This does not modify OpenClaw package code. It only updates user config. - A timestamped backup of config is written before changes. - If no local GGUF exists, install proceeds by pulling the selected model from Ollama. - Model names are normalized with `:latest` tag for consistent Ollama interaction. - If embedding model changes, rebuild/re-embed existing memory vectors to avoid retrieval mismatch across incompatible vector spaces. - With `--reindex-memory auto`, installer reindexes only when the effective embedding fingerprint changed (`provider`, `model`, `baseUrl`, `apiKey presence`). - Drift checks require a non-empty apiKey but do not require a literal `"ollama"` value. - Config backups are created only when a write is needed. - Legacy schema fallback is supported: if `agents.defaults.memorySearch` is absent, the enforcer reads known legacy paths and mirrors writes to preserve compatibility.

path: LICENSE.md
content: MIT License

Copyright (c) 2026 vidarbrekke

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


path: uninstall.sh
content: #!/usr/bin/env bash
# Revert installer-managed changes where possible.
set -euo pipefail
IFS=$'\n\t'

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMMON_SH="${SKILL_DIR}/lib/common.sh"
if [ -f "${COMMON_SH}" ]; then
  # shellcheck source=/dev/null
  source "${COMMON_SH}"
fi

OPENCLAW_DIR="${HOME}/.openclaw"
CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-${OPENCLAW_DIR}/openclaw.json}"
RESTART_GATEWAY="no" # yes|no
UNINSTALL_WATCHDOG="yes" # yes|no

usage() {
  cat <<'EOF'
Usage:
  uninstall.sh [options]

Options:
  --openclaw-config <path>  OpenClaw config path (default: ~/.openclaw/openclaw.json)
  --restart-gateway <mode>  yes | no (default: no)
  --uninstall-watchdog <m>  yes | no (default: yes)
  --help                    show help
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
    --openclaw-config) CONFIG_PATH="$2"; shift 2 ;;
    --restart-gateway) RESTART_GATEWAY="$2"; shift 2 ;;
    --uninstall-watchdog) UNINSTALL_WATCHDOG="$2"; shift 2 ;;
    --help|-h) usage; exit 0 ;;
    *) echo "Unknown option: $1"; usage; exit 1 ;;
  esac
done

case "$RESTART_GATEWAY" in yes|no) ;; *) echo "Invalid --restart-gateway"; exit 1 ;; esac
case "$UNINSTALL_WATCHDOG" in yes|no) ;; *) echo "Invalid --uninstall-watchdog"; exit 1 ;; esac

echo "Uninstalling ollama-memory-embeddings changes..."

if [ "$UNINSTALL_WATCHDOG" = "yes" ] && [ -x "${SKILL_DIR}/watchdog.sh" ]; then
  if [ "$(uname -s)" = "Darwin" ]; then
    "${SKILL_DIR}/watchdog.sh" --uninstall-launchd || true
  fi
fi

if [ -f "$CONFIG_PATH" ]; then
  latest_backup="$(ls -1t "${CONFIG_PATH}".bak.* 2>/dev/null | head -n1 || true)"
  if [ -n "$latest_backup" ]; then
    cp "$latest_backup" "$CONFIG_PATH"
    echo "Restored config from backup: $latest_backup"
  else
    echo "No config backup found. Leaving config as-is: $CONFIG_PATH"
  fi
else
  echo "Config not found: $CONFIG_PATH"
fi

if [ "$RESTART_GATEWAY" = "yes" ] && command -v openclaw >/dev/null 2>&1; then
  openclaw gateway restart || true
fi

echo "Done."


path: verify.sh
content: #!/usr/bin/env bash
# Verify Ollama embeddings endpoint with selected model.
# Checks: model exists in Ollama → endpoint reachable → valid embedding response.
set -euo pipefail
IFS=$'\n\t'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMMON_SH="${SCRIPT_DIR}/lib/common.sh"
CONFIG_CLI="${SCRIPT_DIR}/lib/config.js"
if [ ! -f "${COMMON_SH}" ]; then
  echo "[ERROR] Missing shared helper: ${COMMON_SH}" >&2
  exit 1
fi
if [ ! -f "${CONFIG_CLI}" ]; then
  echo "[ERROR] Missing config helper: ${CONFIG_CLI}" >&2
  exit 1
fi
source "${COMMON_SH}"

MODEL=""
BASE_URL=""
CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-${HOME}/.openclaw/openclaw.json}"
VERBOSE=0
TMP_BODY=""
TMP_ERR=""

usage() {
  cat <<'EOF'
Usage:
  verify.sh [--model <id>] [--base-url <url>] [--openclaw-config <path>] [--verbose]

Verifies that the configured Ollama embeddings endpoint is working correctly.

Behavior:
  - If --model is omitted, reads memorySearch.model from OpenClaw config.
  - If --base-url is omitted, reads memorySearch.remote.baseUrl from config,
    then defaults to http://127.0.0.1:11434/v1/
  - Checks: (1) model exists in Ollama, (2) endpoint returns valid embedding.
  - Use --verbose to dump raw API response on failure.
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
    --model) MODEL="$2"; shift 2 ;;
    --base-url) BASE_URL="$2"; shift 2 ;;
    --openclaw-config) CONFIG_PATH="$2"; shift 2 ;;
    --verbose) VERBOSE=1; shift ;;
    --help|-h) usage; exit 0 ;;
    *) echo "Unknown option: $1"; usage; exit 1 ;;
  esac
done

cleanup() {
  if [ -n "${TMP_BODY:-}" ] && [ -f "${TMP_BODY:-}" ]; then
    rm -f "${TMP_BODY}"
  fi
  if [ -n "${TMP_ERR:-}" ] && [ -f "${TMP_ERR:-}" ]; then
    rm -f "${TMP_ERR}"
  fi
}
trap cleanup EXIT INT TERM

require_cmd node
require_cmd curl

# ── Read config if needed ────────────────────────────────────────────────────

if [ -z "$MODEL" ] || [ -z "$BASE_URL" ]; then
  MAP_OUTPUT="$(node "${CONFIG_CLI}" resolve-model-base "${CONFIG_PATH}")"
  CFG_MODEL="$(printf "%s\n" "$MAP_OUTPUT" | sed -n '1p')"
  CFG_BASE_URL="$(printf "%s\n" "$MAP_OUTPUT" | sed -n '2p')"
  [ -z "$MODEL" ] && MODEL="$CFG_MODEL"
  [ -z "$BASE_URL" ] && BASE_URL="$CFG_BASE_URL"
fi

if [ -z "$MODEL" ]; then
  echo "ERROR: Could not determine embedding model."
  echo "  Provide --model <id> or configure memorySearch.model in ${CONFIG_PATH}"
  exit 1
fi

# Normalize model tag
MODEL="$(normalize_model "$MODEL")"

# Normalize URL to .../v1 and call /embeddings
BASE_URL="${BASE_URL%/}"
if [[ "$BASE_URL" != */v1 ]]; then
  BASE_URL="${BASE_URL}/v1"
fi
EMBED_URL="${BASE_URL}/embeddings"

echo "Checking Ollama embeddings:"
echo "  URL:   ${EMBED_URL}"
echo "  Model: ${MODEL}"

# ── Step 1: Check model exists in Ollama ─────────────────────────────────────

echo ""
echo "  [1/2] Checking model availability in Ollama..."
if command -v ollama >/dev/null 2>&1; then
  if ! ollama list 2>/dev/null | awk 'NR>1{print $1}' | grep -qFx "${MODEL}" 2>/dev/null && \
     ! ollama list 2>/dev/null | awk 'NR>1{print $1}' | grep -qFx "${MODEL%%:*}" 2>/dev/null; then
    echo "  WARNING: model '${MODEL}' not found in 'ollama list'."
    echo "  The model may not be pulled. Try: ollama pull ${MODEL%%:*}"
    echo ""
    echo "  Continuing with endpoint check anyway..."
  else
    echo "  Model '${MODEL}' found in Ollama."
  fi
else
  echo "  NOTE: 'ollama' CLI not in PATH; skipping model existence check."
fi

# ── Step 2: Call embeddings endpoint ─────────────────────────────────────────

echo "  [2/2] Calling embeddings endpoint..."

PAYLOAD=$(cat <<EOF
{"model":"${MODEL}","input":"openclaw memory embeddings health check"}
EOF
)

HTTP_CODE=""
RESP=""

# Capture HTTP status and response body without mixing stderr into status code.
TMP_BODY="$(mktemp)"
TMP_ERR="$(mktemp)"
set +e
HTTP_CODE="$(curl -sS -o "$TMP_BODY" -w "%{http_code}" \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD" \
  "$EMBED_URL" 2>"$TMP_ERR")"
CURL_STATUS=$?
set -e

RESP="$(<"$TMP_BODY")"
CURL_ERR="$(<"$TMP_ERR")"

if [ "$CURL_STATUS" -ne 0 ]; then
  echo "  ERROR: curl failed to reach ${EMBED_URL}"
  echo "  Is Ollama running? Check: curl http://127.0.0.1:11434/api/tags"
  if [ "$VERBOSE" -eq 1 ] && [ -n "$CURL_ERR" ]; then
    echo "  curl error: ${CURL_ERR}"
  fi
  exit 1
fi

if [ "$HTTP_CODE" != "200" ]; then
  echo "  ERROR: embeddings endpoint returned HTTP ${HTTP_CODE}"
  if [ "$VERBOSE" -eq 1 ] && [ -n "$RESP" ]; then
    echo ""
    echo "  Raw response (first 2000 chars):"
    echo "  ${RESP:0:2000}"
  fi
  # Try to extract error message
  if [ -n "$RESP" ]; then
    ERR_MSG="$(echo "$RESP" | node -e '
      let d=""; process.stdin.on("data",c=>d+=c); process.stdin.on("end",()=>{
        try { const j=JSON.parse(d); console.log(j.error||j.message||""); } catch { console.log(""); }
      });
    ' 2>/dev/null)" || true
    if [ -n "$ERR_MSG" ]; then
      echo "  Server error: ${ERR_MSG}"
    fi
  fi
  exit 1
fi

node - "$TMP_BODY" "$VERBOSE" <<'NODEOF'
const fs = require("fs");
const bodyPath = process.argv[2];
const verbose = process.argv[3] === "1";
const raw = fs.readFileSync(bodyPath, "utf8");
let body;
try { body = JSON.parse(raw); } catch {
  console.error("  ERROR: embeddings endpoint did not return valid JSON.");
  if (verbose) {
    console.error("  Raw response (first 2000 chars):");
    console.error("  " + raw.slice(0, 2000));
  }
  process.exit(1);
}

const arr = body?.data?.[0]?.embedding;
if (!Array.isArray(arr) || arr.length === 0) {
  console.error("  ERROR: embeddings response missing data[0].embedding.");
  console.error("  Top-level keys: " + Object.keys(body).join(", "));
  if (verbose) {
    console.error("  Raw response (first 2000 chars):");
    console.error("  " + raw.slice(0, 2000));
  }
  process.exit(1);
}
console.log(`  OK: received embedding vector (dims=${arr.length})`);
NODEOF


path: VERSION.txt
content: 1.0.4


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)