diff --git a/.devcontainer/globalState.json b/.devcontainer/cline-globalState.json similarity index 100% rename from .devcontainer/globalState.json rename to .devcontainer/cline-globalState.json diff --git a/.devcontainer/secrets.json b/.devcontainer/cline-secrets.json similarity index 100% rename from .devcontainer/secrets.json rename to .devcontainer/cline-secrets.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 192b2db4..dfd6f40d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,5 +6,5 @@ } }, "postCreateCommand": "bash ./.devcontainer/post-create-cmd.sh > /tmp/post-create-cmd.log", - "postStartCommand": "bash ./.devcontainer/post-start-cmd.sh > /tmp/post-start-cmd.log" + "postStartCommand": "bash ./.devcontainer/start-hermes.sh > /tmp/post-start-cmd.log" } diff --git a/.devcontainer/post-create-cmd.sh b/.devcontainer/post-create-cmd.sh index a4745c59..b2fc843d 100755 --- a/.devcontainer/post-create-cmd.sh +++ b/.devcontainer/post-create-cmd.sh @@ -1,73 +1,166 @@ #!/bin/bash -# Install modelrelay globally -sudo npm install modelrelay -g --prefix /usr/local/lib/modelrelay -sudo ln -sf /usr/local/lib/modelrelay/bin/modelrelay /usr/local/bin/modelrelay -sudo npm cache clean --force +HERMES_VERSION="v2026.7.7.2" +OMNIROUTE_VERSION=3.8.48 +MODELRELAY_VERSION=1.18.0 +OLLAMA_VERSION=0.32.1 +NODE_VERSION=24.18.0 +MNEMON_VERSION=0.1.17 -echo "[post-create-cmd.sh] Checking modelrelay..." -if command -v modelrelay &>/dev/null; then - if pgrep -f modelrelay > /dev/null; then - echo "[post-create-cmd.sh] modelrelay is already running, skipping" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT_PATH="${BASH_SOURCE[0]}" +SCRIPT_NAME="$(basename -- "$SCRIPT_PATH")" + +sudo apt-get update && sudo apt-get install -y zsh ripgrep && sudo rm -rf /var/lib/apt/lists/* + +# InstallNode.js binaries and libraries + +# Install the Ollama binary from the official image +curl -fsSL https://ollama.com/install.sh | sh + +echo "[$SCRIPT_NAME] Checking ollama..." +if command -v ollama &>/dev/null; then + if pgrep -f ollama > /dev/null; then + echo "[$SCRIPT_NAME] ollama is already running, skipping" else - echo "[post-create-cmd.sh] Starting modelrelay in the background..." - setsid /usr/local/bin/modelrelay >> /tmp/modelrelay.log 2>&1 & + echo "[$SCRIPT_NAME] Starting ollama in the background..." + setsid /usr/local/bin/ollama serve >> /tmp/ollama.log 2>&1 & + ( sleep 60 && ollama pull nomic-embed-text >> /tmp/ollama-pull.log 2>&1 ) & fi else - echo "[post-create-cmd.sh] modelrelay not found, skipping start" + echo "[$SCRIPT_NAME] ollama not found, skipping start" fi -# Install Cline with default configuration -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -echo "[post-create-cmd.sh] Installing Cline with default configuration..." -mkdir -p "$HOME/.cline/data" -cp "${SCRIPT_DIR}/globalState.json" "$HOME/.cline/data/globalState.json" -cp "${SCRIPT_DIR}/secrets.json" "$HOME/.cline/data/secrets.json" -bash -c 'code --force --install-extension saoudrizwan.claude-dev' -npm install -g cline # Install ripgrep for better search performance in hermes-agent -RIPGREP_VERSION=15.1.0 -if ! command -v rg &>/dev/null; then - echo "[post-create-cmd.sh] Installing ripgrep for better search performance in hermes-agent..." - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - # Linux - cd /tmp - curl -LO https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep_${RIPGREP_VERSION}-1_amd64.deb - sudo dpkg -i ripgrep_${RIPGREP_VERSION}-1_amd64.deb - rm ripgrep_${RIPGREP_VERSION}-1_amd64.deb - fi -fi +# RIPGREP_VERSION=15.1.0 +# if ! command -v rg &>/dev/null; then +# echo "[$SCRIPT_NAME] Installing ripgrep for better search performance in hermes-agent..." +# if [[ "$OSTYPE" == "linux-gnu"* ]]; then +# # Linux +# cd /tmp +# curl -LO https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep_${RIPGREP_VERSION}-1_amd64.deb +# sudo dpkg -i ripgrep_${RIPGREP_VERSION}-1_amd64.deb +# rm ripgrep_${RIPGREP_VERSION}-1_amd64.deb +# fi +# fi # Install hermes-agent -HERMES_VERSION="v2026.5.7" if ! command -v hermes &>/dev/null; then - echo "[post-create-cmd.sh] Installing hermes-agent ${HERMES_VERSION}..." + echo "[$SCRIPT_NAME] Installing hermes-agent ${HERMES_VERSION}..." curl -fsSL "https://raw.githubusercontent.com/NousResearch/hermes-agent/${HERMES_VERSION}/scripts/install.sh" | bash -s -- --skip-setup npm cache clean --force sudo rm -rf /var/lib/apt/lists/* fi # Ensure agent-client-protocol (ACP) is installed -echo "[post-create-cmd.sh] Checking agent-client-protocol (ACP)..." +echo "[$SCRIPT_NAME] Checking agent-client-protocol (ACP)..." if command -v hermes &>/dev/null; then HERMES_VENV_PYTHON="$HOME/.hermes/hermes-agent/venv/bin/python" if [ -x "$HERMES_VENV_PYTHON" ]; then if ! "$HERMES_VENV_PYTHON" -c "import agent_client_protocol" 2>/dev/null; then - echo "[post-create-cmd.sh] ACP not found, installing..." + echo "[$SCRIPT_NAME] ACP not found, installing..." "$HERMES_VENV_PYTHON" -m pip install "agent-client-protocol>=0.9.0,<1.0" else - echo "[post-create-cmd.sh] ACP already installed" + echo "[$SCRIPT_NAME] ACP already installed" fi fi else - echo "[post-create-cmd.sh] hermes not found, skipping ACP check" + echo "[$SCRIPT_NAME] hermes not found, skipping ACP check" fi # Configure hermes defaults if first run if command -v hermes &>/dev/null && [ -d "$HOME/.hermes/sessions" ] && [ -z "$(ls -A "$HOME/.hermes/sessions")" ]; then - echo "[post-create-cmd.sh] No sessions found, setting up default configuration for custom provider" - hermes config set model.provider custom - hermes config set model.base_url http://localhost:7352/v1 + echo "[$SCRIPT_NAME] No sessions found, setting up default configuration for custom provider" + echo "[$SCRIPT_NAME] Initializing hermes config..." + hermes config set model.default auto-fastest + hermes config set model.provider omniroute + hermes config set providers.omniroute.base_url http://localhost:20128/v1 + hermes config set providers.omniroute.api_key no-key-needed + hermes config set providers.modelrelay.base_url http://localhost:7352/v1 + hermes config set providers.modelrelay.api_key no-key-needed + hermes config set fallback_providers.provider modelrelay + hermes config set fallback_providers.model auto-fastest + + hermes config set auxiliary.title_generation.model auto-fastest + hermes config set auxiliary.title_generation.provider modelrelay + hermes config set auxiliary.vision.model auto-fastest + hermes config set auxiliary.vision.provider modelrelay + hermes config set auxiliary.compression.model auto-fastest + hermes config set auxiliary.compression.provider modelrelay + + # Turn off approval alert and live dangerously since u are in a self-contained container. + hermes config set approvals.mode off + # Turn on memory by default and to mnemon + hermes config set memory.memory_enabled true + hermes config set memory.user_profile_enabled true + hermes config set memory.provider mnemon + # optimize for kanban + hermes config set agent.max_turns 120 + hermes config set kanban.failure_limit 3 + + # Populate default skill and .hermes.md + echo "[$SCRIPT_NAME] Installing Skill: memory-automation.md" + mkdir -p "$HOME/.hermes/skills/memory-automation" + cp ${SCRIPT_DIR}/skill-memory-automation.md "$HOME/.hermes/skills/memory-automation/SKILL.md" + +fi + +# Install modelrelay globally +# sudo npm install -g modelrelay@${MODELRELAY_VERSION} && \ +sudo npm install github:gitricko/modelrelay -g --prefix /usr/local/lib/modelrelay +sudo ln -sf /usr/local/lib/modelrelay/bin/modelrelay /usr/local/bin/modelrelay +sudo npm cache clean --force + +echo "[$SCRIPT_NAME] Checking modelrelay..." +if command -v modelrelay &>/dev/null; then + if pgrep -f modelrelay > /dev/null; then + echo "[$SCRIPT_NAME] modelrelay is already running, skipping" + else + echo "[$SCRIPT_NAME] Starting modelrelay in the background..." + modelrelay --disable + setsid /usr/local/bin/modelrelay >> /tmp/modelrelay.log 2>&1 & + fi +else + echo "[$SCRIPT_NAME] modelrelay not found, skipping start" fi + +# Install OmniRoute and start automatically when desktop loads +sudo npm install omniroute@${OMNIROUTE_VERSION} -g --prefix /usr/local/lib/omniroute +sudo ln -sf /usr/local/lib/omniroute/bin/omniroute /usr/local/bin/omniroute +sudo npm cache clean --force +# sudo mkdir -p /usr/local/lib/node_modules/omniroute/app/logs/application + +echo "[$SCRIPT_NAME] Checking omniroute..." +if command -v omniroute &>/dev/null; then + if pgrep -f omniroute > /dev/null; then + echo "[$SCRIPT_NAME] omniroute is already running, skipping" + else + echo "[$SCRIPT_NAME] Starting omniroute in the background..." + setsid /usr/local/bin/omniroute >> /tmp/omniroute.log 2>&1 & + fi +else + echo "[$SCRIPT_NAME] omniroute not found, skipping start" +fi + + +# Install TailScale +sudo mkdir -p /var/run/tailscale /var/lib/tailscale && sudo curl -fsSL https://tailscale.com/install.sh | sh && sudo rm -rf /var/lib/apt/lists/* + +# Install mnemon +MNEMON_ARCH=amd64 +curl -sL "https://github.com/mnemon-dev/mnemon/releases/download/v${MNEMON_VERSION}/mnemon_${MNEMON_VERSION}_linux_${MNEMON_ARCH}.tar.gz" -o /tmp/mnemon.tar.gz +tar xzf /tmp/mnemon.tar.gz -C /tmp +sudo cp /tmp/mnemon /usr/local/bin/mnemon +sudo chmod +x /usr/local/bin/mnemon +rm -rf /tmp/mnemon.tar.gz /tmp/mnemon + +# Install Cline with default configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +echo "[$SCRIPT_NAME] Installing Cline with default configuration..." +mkdir -p "$HOME/.cline/data" +cp "${SCRIPT_DIR}/cline-globalState.json" "$HOME/.cline/data/globalState.json" +cp "${SCRIPT_DIR}/cline-secrets.json" "$HOME/.cline/data/secrets.json" +bash -c 'code --force --install-extension saoudrizwan.claude-dev' +npm install -g cline diff --git a/.devcontainer/self-check.sh b/.devcontainer/self-check.sh new file mode 100755 index 00000000..038d2bb2 --- /dev/null +++ b/.devcontainer/self-check.sh @@ -0,0 +1,427 @@ +#!/bin/bash +# self-check.sh — Hermes-WebTop boot-time health diagnostics +# +# Probes all components of the hermes-webtop stack and produces a +# human-readable health report + machine-parseable JSON summary. +# +# Auto-discovers Telegram delivery from Hermes config.yaml — no +# separate env vars needed. Falls back to stdout-only if Telegram +# is not configured (the Codespaces / first-time-user default). +# +# Behavioural env vars (thresholds only): +# HERMES_WEBTOP_SKIP_CHECKS — comma-separated check names to skip +# HERMES_WEBTOP_DISK_WARN_PCT — disk % threshold for warning (default: 85) +# HERMES_WEBTOP_CRITICAL_SERVICES— ports whose failure → exit 2 (default: all 4) +# +# Exit codes: +# 0 — all checks passed (or only warnings) +# 1 — one or more warnings (disk, cron, etc.) +# 2 — one or more critical failures (service down, config broken) + +set -euo pipefail + +# ── Config ────────────────────────────────────────────────────────────────── +SKIP_CHECKS="${HERMES_WEBTOP_SKIP_CHECKS:-}" +DISK_WARN_PCT="${HERMES_WEBTOP_DISK_WARN_PCT:-85}" +CRITICAL_SERVICES="${HERMES_WEBTOP_CRITICAL_SERVICES:-7352 20128}" + +HERMES_CONFIG="${HERMES_CONFIG:-$HOME/.hermes/config.yaml}" +HERMES_GATEWAY_URL="${HERMES_GATEWAY_URL:-http://localhost:9119}" +REPORT_FILE="/tmp/health-report.json" + +# Colours (disabled if stderr is not a terminal, e.g. CI) +if [ -t 2 ]; then + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + RED='\033[0;31m' + BOLD='\033[1m' + NC='\033[0m' +else + GREEN=''; YELLOW=''; RED=''; BOLD=''; NC='' +fi + +# ── State ──────────────────────────────────────────────────────────────────── +CRITICAL=0 +WARNINGS=0 +JSON_RESULTS='[]' # JSON array (accumulating) +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +# ── Helpers ────────────────────────────────────────────────────────────────── +_ok() { local label="$1" msg="$2"; printf " ${GREEN}✅${NC} %-18s %s\n" "$label" "$msg"; } +_warn() { local label="$1" msg="$2"; printf " ${YELLOW}⚠️ ${NC}%-18s %s\n" "$label" "$msg"; ((WARNINGS++)) || true; } +_fail() { local label="$1" msg="$2"; printf " ${RED}❌${NC} %-18s %s\n" "$label" "$msg"; ((CRITICAL++)) || true; } + +json_add() { + # json_add [detail_json] + local name="$1" status="$2" message="$3" detail="${4:-null}" + JSON_RESULTS=$(echo "$JSON_RESULTS" | python3 -c " +import json,sys +results = json.loads(sys.stdin.read()) +results.append({ + 'name': '$name', + 'status': '$status', + 'message': '$(echo "$message" | sed "s/'/\\\\'/g")', + 'detail': $detail +}) +print(json.dumps(results)) +") +} + +should_skip() { + local name="$1" + [ -z "$SKIP_CHECKS" ] && return 1 # don't skip + for s in $(echo "$SKIP_CHECKS" | tr ',' ' '); do + [ "$s" = "$name" ] && return 0 + done + return 1 +} + +section() { + echo "" + echo " ${BOLD}$1${NC}" + echo " ───────────────────────────────────────────────" +} + +# ── Checks ─────────────────────────────────────────────────────────────────── + +echo "" +echo " ════════════════════════════════════════════════════════════" +echo " ${BOLD}HERMES-WEBTOP HEALTH REPORT${NC}" +echo " $(date -u)" +echo " ════════════════════════════════════════════════════════════" + +# ── 1. Services ────────────────────────────────────────────────────────────── +section "Services" + +if ! should_skip "services"; then + # Poll all service ports until all respond or timeout + PORT_POLL_TIMEOUT=60 + POLL_STARTED_AT=$(date +%s) + declare -A RESPONDED=([3000]="" [8888]="" [7352]="" [20128]="", [9119]="") + + while true; do + NOW=$(date +%s) + ELAPSED=$((NOW - POLL_STARTED_AT)) + + # Collecting responses + if [ "$ELAPSED" -gt "$PORT_POLL_TIMEOUT" ]; then + for pair in "7352:ModelRelay" "20128:OmniRoute", "9119:HermesGateway"; do + PORT="${pair%%:*}" + NAME="${pair##*:}" + if [ "${RESPONDED[$PORT]}" != "true" ]; then + echo " $NAME (:$PORT) — ❌ never responded" + fi + done + break + fi + + # Testing ports + for pair in "7352:ModelRelay" "20128:OmniRoute" "9119:HermesGateway"; do + PORT="${pair%%:*}" + NAME="${pair##*:}" + + if [ "${RESPONDED[$PORT]}" = "true" ]; then + continue + fi + + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost:${PORT}" 2>/dev/null || HTTP_CODE="000") + HTTP_CODE=$(echo "$HTTP_CODE" | tr -d '[:space:]') + + if [ -n "$HTTP_CODE" ] && [ "$HTTP_CODE" != "000" ]; then + echo " $NAME (:$PORT) — ✅ HTTP ${HTTP_CODE} (${ELAPSED}s)" + RESPONDED[$PORT]="true" + fi + done + + # Check if all ports responded + ALL_RESPONDED=true + for port in 7352 20128 9119; do + if [ "${RESPONDED[$port]}" != "true" ]; then + ALL_RESPONDED=false + break + fi + done + + if [ "$ALL_RESPONDED" = "true" ]; then + break + fi + + sleep 5 + done + + if [ "$ALL_RESPONDED" = "true" ]; then + echo "" + _ok "Services Ports" "responding within ${ELAPSED}s" + else + echo "" + _fail "Services Ports" "failed to respond within ${PORT_POLL_TIMEOUT}s" + fi + +else + echo " (skipped)" +fi + +# ── 2. Models ──────────────────────────────────────────────────────────────── +section "Models" + +if ! should_skip "models"; then + models_json=$(curl -s --max-time 5 "http://localhost:20128/v1/models" 2>/dev/null || echo '{}') + model_count=$(echo "$models_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('data',[])))" 2>/dev/null || echo "0") + # Try to get the default combo name from Hermes config + default_model=$(grep -A1 '^model:' "$HERMES_CONFIG" 2>/dev/null | grep 'default' | head -1 | sed 's/.*default: *//' || echo "unknown") + default_model="${default_model:-unknown}" + + if [ "$model_count" -gt 0 ] 2>/dev/null; then + _ok "OmniRoute" "${model_count} models available (default: ${default_model})" + json_add "models" "ok" "${model_count} models, default combo: ${default_model}" "{\"count\":${model_count},\"default\":\"${default_model}\"}" + else + _warn "OmniRoute" "no models returned from /v1/models (may still be starting)" + json_add "models" "warn" "no models returned (may still be booting)" "{\"count\":0}" + fi +else + echo " (skipped)" +fi + +# ── 3. Mnemon ──────────────────────────────────────────────────────────────── +section "Mnemon" + +if ! should_skip "mnemon"; then + mnemon_version=$(mnemon --version 2>/dev/null || echo "not-found") + mnemon_db=$(find "$HOME" -maxdepth 3 -name ".mnemon" -type d 2>/dev/null | head -1) || true + + if [ "$mnemon_version" != "not-found" ]; then + _ok "Binary" "${mnemon_version}" + json_add "mnemon:binary" "ok" "mnemon ${mnemon_version}" "{\"version\":\"${mnemon_version}\"}" + else + _fail "Binary" "mnemon not found in PATH" + json_add "mnemon:binary" "fail" "mnemon binary not found" "{}" + fi + + if [ -n "$mnemon_db" ]; then + _ok "Database" "found at ${mnemon_db}" + json_add "mnemon:db" "ok" "mnemon db at ${mnemon_db}" "{\"path\":\"${mnemon_db}\"}" + else + _warn "Database" "no .mnemon directory found (will be created on first use)" + json_add "mnemon:db" "warn" "no .mnemon directory yet" "{}" + fi +else + echo " (skipped)" +fi + +# ── 4. Hermes config ───────────────────────────────────────────────────────── +section "Hermes" + +if ! should_skip "hermes"; then + if [ -f "$HERMES_CONFIG" ]; then + cfg_model=$(grep -A1 '^model:' "$HERMES_CONFIG" 2>/dev/null | grep 'default' | head -1 | sed 's/.*default: *//' || echo "") + cfg_provider=$(grep -A1 '^model:' "$HERMES_CONFIG" 2>/dev/null | grep 'provider' | head -1 | sed 's/.*provider: *//' || echo "") + has_gateway=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$HERMES_GATEWAY_URL" 2>/dev/null || true) + has_gateway="${has_gateway:-000}" + + if [ -n "$cfg_model" ]; then + _ok "Config" "model=${cfg_model}, provider=${cfg_provider:-unset}" + json_add "hermes:config" "ok" "config valid: model=$cfg_model, provider=$cfg_provider" "{\"model\":\"${cfg_model}\",\"provider\":\"${cfg_provider}\"}" + else + _warn "Config" "model not set in config (may be fresh install)" + json_add "hermes:config" "warn" "model not configured" "{}" + fi + + if [ "$has_gateway" != "000" ]; then + _ok "Gateway" "HTTP ${has_gateway} at ${HERMES_GATEWAY_URL}" + json_add "hermes:gateway" "ok" "gateway responded HTTP ${has_gateway}" "{\"http_code\":${has_gateway}}" + else + _fail "Gateway" "no response from ${HERMES_GATEWAY_URL}" + json_add "hermes:gateway" "fail" "gateway unreachable" "{}" + fi + else + _fail "Config" "no config at ${HERMES_CONFIG}" + json_add "hermes:config" "fail" "hermes config file not found" "{}" + fi +else + echo " (skipped)" +fi + +# ── 5. Disk ────────────────────────────────────────────────────────────────── +section "Disk" + +if ! should_skip "disk"; then + disk_raw=$(df $HOME 2>/dev/null | tail -1 || true) + if [ -n "$disk_raw" ]; then + disk_pct=$(echo "$disk_raw" | awk '{print $5}' | tr -d '%') + disk_avail=$(echo "$disk_raw" | awk '{print $4}') + # Convert to human-readable if block size is 1K + if [[ "$disk_avail" =~ ^[0-9]+$ ]]; then + disk_avail_gb=$(( disk_avail / 1024 / 1024 )) + disk_total_gb=$(( $(echo "$disk_raw" | awk '{print $2}') / 1024 / 1024 )) + else + disk_avail_gb="$disk_avail" + disk_total_gb="?" + fi + + if [ "$disk_pct" -ge 95 ] 2>/dev/null; then + _fail "Usage" "${disk_pct}% used (${disk_avail_gb}G available) — CRITICAL" + json_add "disk" "fail" "${disk_pct}% used, ${disk_avail_gb}G free" "{\"used_pct\":${disk_pct},\"available_gb\":${disk_avail_gb}}" + elif [ "$disk_pct" -ge "$DISK_WARN_PCT" ] 2>/dev/null; then + _warn "Usage" "${disk_pct}% used (${disk_avail_gb}G available) — threshold: ${DISK_WARN_PCT}%" + json_add "disk" "warn" "${disk_pct}% used, ${disk_avail_gb}G free" "{\"used_pct\":${disk_pct},\"available_gb\":${disk_avail_gb},\"threshold\":${DISK_WARN_PCT}}" + else + _ok "Usage" "${disk_pct}% used (${disk_avail_gb}G available)" + json_add "disk" "ok" "${disk_pct}% used, ${disk_avail_gb}G free" "{\"used_pct\":${disk_pct},\"available_gb\":${disk_avail_gb}}" + fi + else + _warn "Usage" "could not read disk stats for /config" + json_add "disk" "warn" "df failed" "{}" + fi +else + echo " (skipped)" +fi + +# ── 6. Memory ──────────────────────────────────────────────────────────────── +section "Memory" + +if ! should_skip "memory"; then + mem_raw=$(free -m 2>/dev/null | grep "^Mem:" || true) + if [ -n "$mem_raw" ]; then + mem_total=$(echo "$mem_raw" | awk '{print $2}') + mem_used=$(echo "$mem_raw" | awk '{print $3}') + mem_pct=$(( mem_used * 100 / mem_total )) + + if [ "$mem_pct" -ge 90 ] 2>/dev/null; then + _warn "Usage" "${mem_pct}% used (${mem_used}M / ${mem_total}M)" + json_add "memory" "warn" "${mem_pct}% used" "{\"used_pct\":${mem_pct},\"used_mb\":${mem_used},\"total_mb\":${mem_total}}" + else + _ok "Usage" "${mem_pct}% used (${mem_used}M / ${mem_total}M)" + json_add "memory" "ok" "${mem_pct}% used" "{\"used_pct\":${mem_pct},\"used_mb\":${mem_used},\"total_mb\":${mem_total}}" + fi + else + _warn "Usage" "could not read memory stats" + json_add "memory" "warn" "free -m failed" "{}" + fi +else + echo " (skipped)" +fi + +# ── 7. Cron ────────────────────────────────────────────────────────────────── +section "Cron" + +if ! should_skip "cron"; then + cron_output=$(hermes cron list 2>/dev/null || true) + cron_count=$(echo "$cron_output" | grep -c "job_id\|active\|pending\|running" 2>/dev/null || echo "0") + if [ "$cron_count" -gt 0 ] 2>/dev/null; then + _ok "Jobs" "${cron_count} registered" + json_add "cron" "ok" "${cron_count} cron jobs registered" "{\"count\":${cron_count}}" + else + # Cron might not be available in this context — don't fail + _ok "Jobs" "none registered (expected on fresh boot)" + json_add "cron" "ok" "no cron jobs yet" "{\"count\":0}" + fi +else + echo " (skipped)" +fi + +# ── Summary ────────────────────────────────────────────────────────────────── +section "Summary" +echo "" +if [ "$CRITICAL" -gt 0 ]; then + echo " ${RED}${BOLD}FAILED${NC} — ${CRITICAL} critical, ${WARNINGS} warning(s)" + EXIT_CODE=2 +elif [ "$WARNINGS" -gt 0 ]; then + echo " ${YELLOW}${BOLD}WARNINGS${NC} — ${WARNINGS} warning(s), 0 critical" + EXIT_CODE=1 +else + echo " ${GREEN}${BOLD}PASSED${NC} — all checks ok" + EXIT_CODE=0 +fi +echo "" + +# ── Write JSON report ──────────────────────────────────────────────────────── +cat > "$REPORT_FILE" </dev/null | grep 'chat_id' | head -1 | sed 's/.*chat_id: *//' | tr -d '" ' | tr -d ' ') || true + if [ -z "$TELEGRAM_CHAT_ID" ]; then + TELEGRAM_CHAT_ID=$(grep -A10 '^telegram:' "$HERMES_CONFIG" 2>/dev/null | grep 'allowed_chats' | head -1 | sed 's/.*allowed_chats: *//' | tr -d '" ' | tr -d ' ') || true + fi +fi + +if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then + # Build a compact summary for Telegram + SUMMARY="${EXIT_CODE}: " + if [ "$CRITICAL" -gt 0 ]; then + SUMMARY+="🔴 ${CRITICAL} critical, ${WARNINGS} warnings" + elif [ "$WARNINGS" -gt 0 ]; then + SUMMARY+="🟡 ${WARNINGS} warnings" + else + SUMMARY+="🟢 all ok" + fi + + TEXT="*Hermes-WebTop Health Report*" + TEXT+="\nStatus: ${SUMMARY}" + TEXT+="\n╰ $(date -u +'%Y-%m-%d %H:%M UTC')" + + # Add critical failure details + if [ "$CRITICAL" -gt 0 ]; then + TEXT+="\n\n*Failures:*" + echo "$JSON_RESULTS" | python3 -c " +import json,sys +results = json.load(sys.stdin) +for r in results: + if r['status'] == 'fail': + print(f'• {r[\"name\"]}: {r[\"message\"]}') +" 2>/dev/null | while IFS= read -r line; do + TEXT+="\n${line}" + done + fi + + # Add warnings summary + if [ "$WARNINGS" -gt 0 ]; then + CRT=$CRITICAL + echo "$JSON_RESULTS" | python3 -c " +import json,sys +results = json.load(sys.stdin) +warns = [r for r in results if r['status'] == 'warn'] +if warns: + print('\\n*Warnings:*') + for w in warns: + print(f'• {w[\"name\"]}: {w[\"message\"]}') +" 2>/dev/null | while IFS= read -r line; do + TEXT+="\n${line}" + done + fi + + # Send via Telegram Bot API + curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + -d "chat_id=${TELEGRAM_CHAT_ID}" \ + -d "text=${TEXT}" \ + -d "parse_mode=Markdown" \ + -d "disable_web_page_preview=true" \ + --max-time 10 >/dev/null 2>&1 && echo " [telegram] notification sent" || echo " [telegram] failed to send" +else + echo " [telegram] not configured — stdout only (set TELEGRAM_BOT_TOKEN and TELEGRAM_HOME_CHANNEL in ~/.hermes/.env)" +fi + +exit "$EXIT_CODE" \ No newline at end of file diff --git a/.devcontainer/skill-memory-automation.md b/.devcontainer/skill-memory-automation.md new file mode 100644 index 00000000..c687e8ae --- /dev/null +++ b/.devcontainer/skill-memory-automation.md @@ -0,0 +1,248 @@ +--- +name: memory-automation +description: "Automated mnemon memory persistence workflow — recall on start, recall before each turn, auto-save after each response via delegated subagent." +version: 1.4.0 +author: hermes-webtop +tags: + - memory + - mnemon + - persistence + - automation +--- + +# Memory Automation + +Automates the persistent-memory workflow for Hermes using Mnemon graph memory. Used by `.hermes.md` instructions to keep every session primed with context. + +## Mnemon Quick Reference + +### Mnemon Tool Signatures + +```python +mnemon_remember(text, category, importance, entities, tags) # Plugin tool — direct call +mnemon_recall(query, intent, limit) # Plugin tool — direct call +mnemon_forget(insight_id) # Plugin tool — direct call +``` + +These are real Hermes plugin tools, not pseudocode. See [Save Patterns](#save-patterns) for when to use them directly vs via delegated subagent. + +### Categories (for `category` parameter) + +| Category | When to use | +|----------|-------------| +| `fact` | Objective information about the user, project, or environment | +| `preference` | User's likes, dislikes, stylistic choices, workflow preferences | +| `decision` | Architectural decisions, tool choices, design decisions | +| `insight` | Non-obvious findings, troubleshooting steps, workarounds | +| `context` | Session-level context that's useful across sessions | +| `general` | Anything that doesn't fit above | + +### Importance Levels + +| Level | When to use | +|-------|-------------| +| 5 | Critical — user identity, core preferences, security constraints | +| 4 | Important — project goals, recurring preferences, key constraints | +| 3 | Normal — useful context, typical insights | +| 2 | Minor — nice-to-know details | +| 1 | Trivial — barely worth saving (use rarely) | + +## Recall Patterns + +### Full Context Load (Session Start) + +```python +# Called by the SessionStart hook +mnemon_recall(query="", intent="GENERAL", limit=20) +``` + +This loads the 20 most relevant past insights into context. No specific query needed — just a broad sweep of everything mnemon has stored. + +### Topic-Specific Recall (Before Responding) + +```python +# If the hook returns nothing but the topic references past work: +mnemon_recall(query="", intent="GENERAL", limit=10) +``` + +Use the user's message topic as the query. Include key terms, project names, and any domain-specific vocabulary. + +### Intent-Targeted Recall + +```python +# When you need to understand WHY a decision was made: +mnemon_recall(query="", intent="WHY", limit=5) + +# When you need to know WHEN something happened: +mnemon_recall(query="", intent="WHEN", limit=5) + +# When you need info about a specific entity: +mnemon_recall(query="", intent="ENTITY", limit=5) +``` + +## Save Patterns + +### Direct Tool Call (Preferred for Simple Saves) + +`mnemon_remember` is a real Hermes plugin tool exposed by the `gitricko/hermes-plugin-mnemon` plugin. Call it directly from your toolset — no subagent, no CLI invocation needed: + +```python +mnemon_remember( + text="User prefers concise responses and bullet points over prose.", + category="preference", + importance=4, + entities=["gitricko"], + tags=["communication-style","user-preference"] +) +``` + +**Use direct calls when:** +- Saving a single fact or preference inline during a response +- The save is straightforward — no extraction, no multi-item batch +- You're already in a turn; just batch the tool call with whatever else you're doing + +The tool wraps the `mnemon` CLI. No MCP server, no subagent. + +### Delegated Save (Bulk or After Complex Responses) + +For bulk saves (3+ items) or when context is tight, delegate to a subagent with `toolsets=["terminal"]`. The subagent uses the `mnemon remember` CLI since plugin tools may not propagate to child sessions: + +```python +delegate_task( + goal="Save insights to mnemon memory", + context="""Save the following to mnemon memory: + +1. text="User prefers bullet-point summaries over paragraphs.", + category="preference", importance=4, + entities=["gitricko"], tags=["communication-style"] + +2. text="The project uses FastAPI with PostgreSQL.", + category="fact", importance=3, + entities=["project-name"], tags=["tech-stack"] + +For each item run: mnemon remember "" \\ + --cat --imp \\ + --entities e1,e2 \\ + --tags "t1,t2" +Binary: /usr/local/bin/mnemon.""", + toolsets=["terminal"] +) +``` + +**Use delegated saves when:** +- Saving 3+ items at once +- The save requires extraction or reasoning (e.g. "pick out all preferences from this dialog") +- You're low on context tokens and want the work offloaded + +### Decision Guide: Direct vs Delegated + +| Condition | Method | Reasoning | +|-----------|--------|-----------| +| 1–2 items, simple | Direct `mnemon_remember()` tool call | Zero overhead — inlined with response | +| 3+ items | Delegate to subagent with CLI | Worth the ~20K token spawn cost for bulk | +| Save requires extraction/analysis | Delegate to subagent | Extraction logic is better offloaded | +| Already doing other tool calls | Batch `mnemon_remember()` with them | Same turn, no extra cost | +| `.hermes.md` says "delegate subagent" (blanket) | Follow the 1–2 / 3+ split above instead | Blanket delegation burns ~20K tokens per trivial save (e.g. "tmr" → timing detail). Proactively update `.hermes.md` to the two-tier system. If user insists on delegation-only, follow their explicit instruction — but default is cost-efficient direct calls. | +| User explicitly asks for delegation | Always delegate, even for simple saves | User preference overrides efficiency — be transparent about the token cost so they can make an informed choice. | + +### Batch Operations with memory() Fallback + +For structured preference data that needs the memory() tool (target='user' or 'memory'): + +```python +memory( + target="user", + operations=[ + {"action": "add", "content": "User prefers dark mode terminals."}, + {"action": "replace", "old_text": "old stale fact", "content": "updated fact"} + ] +) +``` + +## When to Auto-Save (After Every Response) + +After every user-facing response, scan the exchange for anything on this list: + +| Signal | What to save | +|--------|-------------| +| User stated a preference | Save as `preference` | +| User made a decision | Save as `decision` | +| User corrected you | Save as `preference` or `fact` | +| You discovered a workaround | Save as `insight` | +| User revealed personal info | Save as `fact`, importance=5 | +| User said "remember this" | Save whatever follows | +| Project architecture detail | Save as `fact` | +| Tool configuration quirk | Save as `insight` | + +**When in doubt, save it.** Extra memories are cheap; missing ones are expensive. + +## What NOT to Save + +- Code the repo already tracks (git history) +- Public API docs or well-known facts +- Transient state ("how are you?", current time) +- Things already in `.hermes.md`, `AGENTS.md`, or other config files + +## Example: Full Turn Cycle + +``` +Session Start → mnemon_recall("", limit=20) → preloaded context + +User: "I prefer using ruff over black for formatting." + → Hook: auto mnemon_recall("ruff black formatting", limit=10) + → Respond + → Direct: mnemon_remember( + text="User prefers ruff over black for Python formatting.", + category="preference", importance=4, + entities=["gitricko"], tags=["formatting","python"] + ) + +User: "Remember the project uses Python 3.13" + → Respond + → Direct: mnemon_remember( + text="Project uses Python 3.13.", + category="fact", importance=4, + entities=["gitricko"], tags=["python","project-setup"] + ) + +User: "Also save these three things: ..." + → Respond + → Delegate: bulk save via subagent (3+ items) +``` + +## Pitfalls + +### `mnemon_remember` IS a Real Tool (Plugin-Exposed) +The function `mnemon_remember(...)` is a real Hermes tool, exposed by the `gitricko/hermes-plugin-mnemon` plugin. Call it directly from your toolset — no CLI needed. + +`mnemon_recall(...)` and `mnemon_forget(...)` may also be available depending on what the plugin exposes. Check your tool list at session start. + +Subagents may NOT inherit plugin-defined tools. If delegating a save, pass `mnemon remember` CLI commands in context and set `toolsets=["terminal"]` so the subagent runs via shell. + +### Real CLI Flags vs Tool Parameters (Subagent Use Only) + +When delegating to a subagent, it runs the `mnemon` CLI — not the plugin tool. The CLI uses different flags than the tool parameters: + +| Tool parameter | CLI flag | Notes | +|---|---|---| +| `text` | positional arg | First argument, not a flag | +| `category` | `--cat` | e.g. `--cat preference` | +| `importance` | `--imp` | e.g. `--imp 4` | +| `entities` | `--entities` | Comma-separated: `entity1,entity2` | +| `tags` | `--tags` | Comma-separated: `tag1,tag2` | + +### Sub-Agent Needs `terminal` Toolset +Delegated saves require `toolsets=["terminal"]` — the sub-agent runs `mnemon` via the shell since plugin tools don't propagate to child sessions. Omitting this or using a wrong toolset leaves the sub-agent with no way to run the CLI. + +### Load This Skill Proactively +This skill must be loaded at the start of every session (via `skill_view(name='memory-automation')`). Do not rely on `.hermes.md` reminders to trigger it — if the skill isn't loaded, you won't follow the workflow. Add it to the start-of-session routine. + +### Don't Confuse `mnemon_remember` with `memory()` +`mnemon_remember()` (plugin tool) → stores to **Mnemon graph DB** — durable, queriable, multi-session. + +`memory()` (built-in Hermes tool) → stores to **agent memory** — injected every turn, bounded at 2.2K chars. + +They target different stores. Use `mnemon_remember()` for Mnemon entries and `memory()` for the Hermes built-in memory. The `[MEMORY]` and `[USER PROFILE]` sections injected at the top of every prompt are rendered from the `memory()` tool's data — they are NOT part of `.hermes.md`. Don't conflate the two tools or their targets. + +### `.hermes.md` Says Delegate But It's Wasting Tokens — What To Do +`.hermes.md` often says "delegate subagent to mnemon_remember" as a blanket instruction. The skill's Decision Guide says direct calls are fine for simple saves. Follow the Decision Guide's cost-aware split instead (direct for 1–2 simple items, delegate for 3+/complex). Propose updating `.hermes.md` to the two-tier system. If the user explicitly prefers delegation-only, defer — but be transparent about the ~20K token cost per spawn so it's an informed choice. \ No newline at end of file diff --git a/.devcontainer/start-hermes.sh b/.devcontainer/start-hermes.sh new file mode 100755 index 00000000..6176892b --- /dev/null +++ b/.devcontainer/start-hermes.sh @@ -0,0 +1,157 @@ +#!/bin/bash +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT_PATH="${BASH_SOURCE[0]}" +SCRIPT_NAME="$(basename -- "$SCRIPT_PATH")" + +echo "[$SCRIPT_NAME] 1. Starting modelrelay..." +if command -v modelrelay &>/dev/null; then + if pgrep -f modelrelay > /dev/null; then + echo "[$SCRIPT_NAME] modelrelay is already running, skipping" + else + echo "[$SCRIPT_NAME] Starting modelrelay in the background..." + setsid /usr/local/bin/modelrelay >> /tmp/modelrelay.log 2>&1 & + fi +else + echo "[$SCRIPT_NAME] modelrelay not found, skipping start" +fi + +echo "[$SCRIPT_NAME] 2. Starting omniroute..." +if command -v omniroute &>/dev/null; then + if pgrep -f omniroute > /dev/null; then + echo "[$SCRIPT_NAME] omniroute is already running, skipping" + else + echo "[$SCRIPT_NAME] Starting omniroute in the background..." + setsid /usr/local/bin/omniroute --no-open --log >> /tmp/omniroute.log 2>&1 & + fi +else + echo "[$SCRIPT_NAME] omniroute not found, skipping start" +fi + +echo "[$SCRIPT_NAME] 3. Starting ollama..." +if command -v ollama &>/dev/null; then + if pgrep -f ollama > /dev/null; then + echo "[$SCRIPT_NAME] ollama is already running, skipping" + else + echo "[$SCRIPT_NAME] Starting ollama in the background..." + setsid /usr/local/bin/ollama serve >> /tmp/ollama.log 2>&1 & + ( sleep 60 && ollama pull nomic-embed-text >> /tmp/ollama-pull.log 2>&1 ) & + fi +else + echo "[$SCRIPT_NAME] ollama not found, skipping start" +fi + + +if [ -d "$HOME/.hermes/logs" ] && [ -z "$(ls -A "$HOME/.hermes/logs")" ]; then + echo "[$SCRIPT_NAME] No logs found in $HOME/.hermes/logs, setting up default configuration for custom provider" + echo "[$SCRIPT_NAME] Initializing hermes config..." + hermes config set model.default auto-fastest + hermes config set model.provider modelrelay + hermes config set providers.omniroute.base_url http://localhost:20128/v1 + hermes config set providers.omniroute.api_key no-key-needed + hermes config set providers.modelrelay.base_url http://localhost:7352/v1 + hermes config set providers.modelrelay.api_key no-key-needed + hermes config set fallback_providers.provider modelrelay + hermes config set fallback_providers.model auto-fastest + + # Turn off approval alert and live dangerously since u are in a self-contained container. + hermes config set approvals.mode off + # Turn on memory by default and to mnemon + hermes config set memory.memory_enabled true + hermes config set memory.user_profile_enabled true + hermes config set memory.provider mnemon + # optimize for kanban + hermes config set agent.max_turns 120 + hermes config set kanban.failure_limit 3 + + # Populate default skill and .hermes.md + echo "[$SCRIPT_NAME] Installing Skill: memory-automation.md" + mkdir -p "$HOME/.hermes/skills/memory-automation" + cp ${SCRIPT_DIR}/skill-memory-automation.md "$HOME/.hermes/skills/memory-automation/SKILL.md" + + echo "[$SCRIPT_NAME] Populate .hermes.md" + cp ${SCRIPT_DIR}/.hermes.md "$HOME/.hermes.md" + +fi + +mkdir -p ~/.hermes/logs + +# Install Telegram gateway dependency if missing +$HOME/.hermes/hermes-agent/venv/bin/python -m ensurepip --upgrade || true +ln -s $HOME/.hermes/hermes-agent/venv/bin/pip3 $HOME/.hermes/hermes-agent/venv/bin/pip || true +$HOME/.hermes/hermes-agent/venv/bin/pip install python-telegram-bot 2>/dev/null || true + +# update mnemon provider if version changes (synced BEFORE gateway starts) +echo "[$SCRIPT_NAME] Checking mnemon provider..." +rm -rf /tmp/mnemon_repo +if git clone https://github.com/gitricko/hermes-plugin-mnemon /tmp/mnemon_repo; then + if [ ! -d "$HOME/.hermes/plugins/mnemon" ] || ! diff -r -q -x __pycache__ "$HOME/.hermes/plugins/mnemon" "/tmp/mnemon_repo/mnemon" >/dev/null 2>&1; then + echo "[$SCRIPT_NAME] Mnemon plugin is missing or out of date. Updating..." + mkdir -p "$HOME/.hermes/plugins" + rm -rf "$HOME/.hermes/plugins/mnemon" + cp -r "/tmp/mnemon_repo/mnemon" "$HOME/.hermes/plugins/mnemon" + echo "[$SCRIPT_NAME] Mnemon plugin updated successfully." + else + echo "[$SCRIPT_NAME] Mnemon plugin is up to date." + fi + rm -rf /tmp/mnemon_repo +else + echo "[$SCRIPT_NAME] WARNING: Failed to clone gitricko/hermes-plugin-mnemon repository." +fi + + # Start Hermes Gateway in background (mnemon is ready before this fires) +echo "[$SCRIPT_NAME] 4. Starting Hermes Gateway..." +if command -v hermes &>/dev/null; then + if pgrep -f 'hermes gateway' > /dev/null; then + echo "[$SCRIPT_NAME] hermes-gateway is already running, skipping" + else + echo "[$SCRIPT_NAME] Starting hermes-gateway in the background..." + setsid hermes gateway run --no-supervise > ~/.hermes/logs/gateway.log 2>&1 & + fi +else + echo "[$SCRIPT_NAME] hermes not found, skipping start" +fi + + # echo "[$SCRIPT_NAME] Starting Hermes Gateway..." + # nohup hermes gateway run --no-supervise > ~/.hermes/logs/gateway.log 2>&1 & + + # Start Hermes Dashboard in background + # echo "[$SCRIPT_NAME] Starting Hermes Dashboard..." + # nohup hermes dashboard --port 9119 --no-open > ~/.hermes/logs/dashboard.log 2>&1 & + +echo "[$SCRIPT_NAME] 5. Starting Hermes Dashboard..." +if command -v hermes &>/dev/null; then + if pgrep -f 'hermes dashboard' > /dev/null; then + echo "[$SCRIPT_NAME] hermes-dashboard is already running, skipping" + else + echo "[$SCRIPT_NAME] Starting hermes-dashboard in the background..." + setsid hermes dashboard --port 9119 --no-open > ~/.hermes/logs/dashboard.log 2>&1 & + fi +else + echo "[$SCRIPT_NAME] hermes not found, skipping start" +fi + +# Remind Hermes on Mnemon setup if needed +if [ ! -f "$HOME/.hermes/memories/USER.md" ]; then + echo "[$SCRIPT_NAME] Setting up Mnemon USER.md..." + echo 'Always use Mnemon (mnemon_remember / mnemon_recall) as primary memory provider instead of the standard memory() tool. Mnemon has no char limit. Only fall back to memory() for structured preference data (target=user or memory).' > "$HOME/.hermes/memories/USER.md" +fi + +# Wait for Hermes dashboard to be ready (replaces brittle sleep 15) +echo "[$SCRIPT_NAME] Waiting for Hermes dashboard to become healthy..." +for i in $(seq 1 20); do + if curl -s -o /dev/null -w "%{http_code}" http://localhost:9119 2>/dev/null | grep -q "200\|302\|401"; then + echo "[$SCRIPT_NAME] Dashboard ready after $((i * 3)) seconds." + break + fi + if [ "$i" -eq 20 ]; then + echo "[$SCRIPT_NAME] WARNING: Dashboard did not respond within 60 seconds. Check ~/.hermes/logs/dashboard.log" + fi + sleep 3 +done + +# All services started and ready +echo "[$SCRIPT_NAME] All hermes-agent services started and ready." + +# Run boot-time health self-check after all services are ready +echo "[$SCRIPT_NAME] Running boot-time health self-check..." +${SCRIPT_DIR}/self-check.sh || echo "[$SCRIPT_NAME] WARNING: self-check reported issues"