9bdd00ea5a
write_observation_feed_config() now uses jq as the primary JSON manipulation tool, falls back to python3, then to node. This gives users the most reliable path regardless of their system tooling. Added 15 new tests covering all three fallback paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1166 lines
37 KiB
Bash
Executable File
1166 lines
37 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
# claude-mem OpenClaw Plugin Installer
|
||
# Installs the claude-mem persistent memory plugin for OpenClaw gateways.
|
||
# Usage: bash install.sh [--non-interactive]
|
||
|
||
###############################################################################
|
||
# Constants
|
||
###############################################################################
|
||
|
||
readonly MIN_BUN_VERSION="1.1.14"
|
||
readonly INSTALLER_VERSION="1.0.0"
|
||
readonly NON_INTERACTIVE="${1:-}"
|
||
|
||
###############################################################################
|
||
# Color utilities — auto-detect terminal color support
|
||
###############################################################################
|
||
|
||
if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]]; then
|
||
readonly COLOR_RED='\033[0;31m'
|
||
readonly COLOR_GREEN='\033[0;32m'
|
||
readonly COLOR_YELLOW='\033[0;33m'
|
||
readonly COLOR_BLUE='\033[0;34m'
|
||
readonly COLOR_MAGENTA='\033[0;35m'
|
||
readonly COLOR_CYAN='\033[0;36m'
|
||
readonly COLOR_BOLD='\033[1m'
|
||
readonly COLOR_RESET='\033[0m'
|
||
else
|
||
readonly COLOR_RED=''
|
||
readonly COLOR_GREEN=''
|
||
readonly COLOR_YELLOW=''
|
||
readonly COLOR_BLUE=''
|
||
readonly COLOR_MAGENTA=''
|
||
readonly COLOR_CYAN=''
|
||
readonly COLOR_BOLD=''
|
||
readonly COLOR_RESET=''
|
||
fi
|
||
|
||
info() { echo -e "${COLOR_BLUE}ℹ${COLOR_RESET} $*"; }
|
||
success() { echo -e "${COLOR_GREEN}✓${COLOR_RESET} $*"; }
|
||
warn() { echo -e "${COLOR_YELLOW}⚠${COLOR_RESET} $*"; }
|
||
error() { echo -e "${COLOR_RED}✗${COLOR_RESET} $*" >&2; }
|
||
|
||
prompt_user() {
|
||
if [[ "$NON_INTERACTIVE" == "--non-interactive" ]]; then
|
||
error "Cannot prompt in non-interactive mode: $*"
|
||
return 1
|
||
fi
|
||
if [[ ! -t 0 ]]; then
|
||
error "Cannot prompt when stdin is not a terminal: $*"
|
||
return 1
|
||
fi
|
||
echo -en "${COLOR_CYAN}?${COLOR_RESET} $* "
|
||
}
|
||
|
||
###############################################################################
|
||
# Banner
|
||
###############################################################################
|
||
|
||
print_banner() {
|
||
echo -e "${COLOR_MAGENTA}${COLOR_BOLD}"
|
||
cat << 'BANNER'
|
||
┌─────────────────────────────────────────┐
|
||
│ claude-mem × OpenClaw │
|
||
│ Persistent Memory Plugin Installer │
|
||
└─────────────────────────────────────────┘
|
||
BANNER
|
||
echo -e "${COLOR_RESET}"
|
||
info "Installer v${INSTALLER_VERSION}"
|
||
echo ""
|
||
}
|
||
|
||
###############################################################################
|
||
# Platform detection
|
||
###############################################################################
|
||
|
||
PLATFORM=""
|
||
IS_WSL=""
|
||
|
||
detect_platform() {
|
||
local uname_out
|
||
uname_out="$(uname -s)"
|
||
|
||
case "${uname_out}" in
|
||
Darwin*)
|
||
PLATFORM="macos"
|
||
;;
|
||
Linux*)
|
||
if grep -qi microsoft /proc/version 2>/dev/null; then
|
||
PLATFORM="linux"
|
||
IS_WSL="true"
|
||
else
|
||
PLATFORM="linux"
|
||
fi
|
||
;;
|
||
MINGW*|MSYS*|CYGWIN*)
|
||
PLATFORM="windows"
|
||
;;
|
||
*)
|
||
error "Unsupported platform: ${uname_out}"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
info "Detected platform: ${PLATFORM}${IS_WSL:+ (WSL)}"
|
||
}
|
||
|
||
###############################################################################
|
||
# Version comparison — returns 0 if $1 >= $2
|
||
###############################################################################
|
||
|
||
version_gte() {
|
||
local v1="$1" v2="$2"
|
||
local -a parts1 parts2
|
||
IFS='.' read -ra parts1 <<< "$v1"
|
||
IFS='.' read -ra parts2 <<< "$v2"
|
||
|
||
for i in 0 1 2; do
|
||
local p1="${parts1[$i]:-0}"
|
||
local p2="${parts2[$i]:-0}"
|
||
if (( p1 > p2 )); then return 0; fi
|
||
if (( p1 < p2 )); then return 1; fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
###############################################################################
|
||
# Bun detection and installation
|
||
# Translated from plugin/scripts/smart-install.js patterns
|
||
###############################################################################
|
||
|
||
BUN_PATH=""
|
||
|
||
find_bun_path() {
|
||
# Try PATH first
|
||
if command -v bun &>/dev/null; then
|
||
BUN_PATH="$(command -v bun)"
|
||
return 0
|
||
fi
|
||
|
||
# Check common installation paths (handles fresh installs before PATH reload)
|
||
local -a bun_paths=(
|
||
"${HOME}/.bun/bin/bun"
|
||
"/usr/local/bin/bun"
|
||
"/opt/homebrew/bin/bun"
|
||
)
|
||
|
||
for candidate in "${bun_paths[@]}"; do
|
||
if [[ -x "$candidate" ]]; then
|
||
BUN_PATH="$candidate"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
BUN_PATH=""
|
||
return 1
|
||
}
|
||
|
||
check_bun() {
|
||
if ! find_bun_path; then
|
||
return 1
|
||
fi
|
||
|
||
# Verify minimum version
|
||
local bun_version
|
||
bun_version="$("$BUN_PATH" --version 2>/dev/null)" || return 1
|
||
|
||
if version_gte "$bun_version" "$MIN_BUN_VERSION"; then
|
||
success "Bun ${bun_version} found at ${BUN_PATH}"
|
||
return 0
|
||
else
|
||
warn "Bun ${bun_version} is below minimum required version ${MIN_BUN_VERSION}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
install_bun() {
|
||
info "Installing Bun runtime..."
|
||
|
||
if ! curl -fsSL https://bun.sh/install | bash; then
|
||
error "Failed to install Bun automatically"
|
||
error "Please install manually:"
|
||
error " curl -fsSL https://bun.sh/install | bash"
|
||
error " Or: brew install oven-sh/bun/bun (macOS)"
|
||
error "Then restart your terminal and re-run this installer."
|
||
exit 1
|
||
fi
|
||
|
||
# Re-detect after install (installer may have placed it in ~/.bun/bin)
|
||
if ! find_bun_path; then
|
||
error "Bun installation completed but binary not found in expected locations"
|
||
error "Please restart your terminal and re-run this installer."
|
||
exit 1
|
||
fi
|
||
|
||
local bun_version
|
||
bun_version="$("$BUN_PATH" --version 2>/dev/null)" || true
|
||
success "Bun ${bun_version} installed at ${BUN_PATH}"
|
||
}
|
||
|
||
###############################################################################
|
||
# uv detection and installation
|
||
# Translated from plugin/scripts/smart-install.js patterns
|
||
###############################################################################
|
||
|
||
UV_PATH=""
|
||
|
||
find_uv_path() {
|
||
# Try PATH first
|
||
if command -v uv &>/dev/null; then
|
||
UV_PATH="$(command -v uv)"
|
||
return 0
|
||
fi
|
||
|
||
# Check common installation paths (handles fresh installs before PATH reload)
|
||
local -a uv_paths=(
|
||
"${HOME}/.local/bin/uv"
|
||
"${HOME}/.cargo/bin/uv"
|
||
"/usr/local/bin/uv"
|
||
"/opt/homebrew/bin/uv"
|
||
)
|
||
|
||
for candidate in "${uv_paths[@]}"; do
|
||
if [[ -x "$candidate" ]]; then
|
||
UV_PATH="$candidate"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
UV_PATH=""
|
||
return 1
|
||
}
|
||
|
||
check_uv() {
|
||
if ! find_uv_path; then
|
||
return 1
|
||
fi
|
||
|
||
local uv_version
|
||
uv_version="$("$UV_PATH" --version 2>/dev/null)" || return 1
|
||
success "uv ${uv_version} found at ${UV_PATH}"
|
||
return 0
|
||
}
|
||
|
||
install_uv() {
|
||
info "Installing uv (Python package manager for Chroma support)..."
|
||
|
||
if ! curl -LsSf https://astral.sh/uv/install.sh | sh; then
|
||
error "Failed to install uv automatically"
|
||
error "Please install manually:"
|
||
error " curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||
error " Or: brew install uv (macOS)"
|
||
error "Then restart your terminal and re-run this installer."
|
||
exit 1
|
||
fi
|
||
|
||
# Re-detect after install
|
||
if ! find_uv_path; then
|
||
error "uv installation completed but binary not found in expected locations"
|
||
error "Please restart your terminal and re-run this installer."
|
||
exit 1
|
||
fi
|
||
|
||
local uv_version
|
||
uv_version="$("$UV_PATH" --version 2>/dev/null)" || true
|
||
success "uv ${uv_version} installed at ${UV_PATH}"
|
||
}
|
||
|
||
###############################################################################
|
||
# OpenClaw gateway detection
|
||
###############################################################################
|
||
|
||
OPENCLAW_PATH=""
|
||
|
||
find_openclaw() {
|
||
# Try PATH first
|
||
if command -v openclaw.mjs &>/dev/null; then
|
||
OPENCLAW_PATH="$(command -v openclaw.mjs)"
|
||
return 0
|
||
fi
|
||
|
||
# Check common installation paths
|
||
local -a openclaw_paths=(
|
||
"${HOME}/.openclaw/openclaw.mjs"
|
||
"/usr/local/bin/openclaw.mjs"
|
||
"/usr/local/lib/node_modules/openclaw/openclaw.mjs"
|
||
"${HOME}/.npm-global/lib/node_modules/openclaw/openclaw.mjs"
|
||
)
|
||
|
||
# Also check for node_modules in common project locations
|
||
if [[ -n "${NODE_PATH:-}" ]]; then
|
||
openclaw_paths+=("${NODE_PATH}/openclaw/openclaw.mjs")
|
||
fi
|
||
|
||
for candidate in "${openclaw_paths[@]}"; do
|
||
if [[ -f "$candidate" ]]; then
|
||
OPENCLAW_PATH="$candidate"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
OPENCLAW_PATH=""
|
||
return 1
|
||
}
|
||
|
||
check_openclaw() {
|
||
if ! find_openclaw; then
|
||
error "OpenClaw gateway not found"
|
||
error ""
|
||
error "The claude-mem plugin requires an OpenClaw gateway to be installed."
|
||
error "Please install OpenClaw first:"
|
||
error ""
|
||
error " npm install -g openclaw"
|
||
error " # or visit: https://openclaw.dev/docs/installation"
|
||
error ""
|
||
error "Then re-run this installer."
|
||
exit 1
|
||
fi
|
||
|
||
success "OpenClaw gateway found at ${OPENCLAW_PATH}"
|
||
}
|
||
|
||
###############################################################################
|
||
# Plugin installation — clone, build, install, enable
|
||
# Flow based on openclaw/Dockerfile.e2e
|
||
###############################################################################
|
||
|
||
CLAUDE_MEM_REPO="https://github.com/thedotmack/claude-mem.git"
|
||
|
||
install_plugin() {
|
||
local build_dir
|
||
build_dir="$(mktemp -d)"
|
||
|
||
# Ensure cleanup on exit from this function
|
||
cleanup_build_dir() {
|
||
if [[ -d "$build_dir" ]]; then
|
||
rm -rf "$build_dir"
|
||
fi
|
||
}
|
||
trap cleanup_build_dir EXIT
|
||
|
||
info "Cloning claude-mem repository..."
|
||
if ! git clone --depth 1 "$CLAUDE_MEM_REPO" "$build_dir/claude-mem" 2>&1; then
|
||
error "Failed to clone claude-mem repository"
|
||
error "Check your internet connection and try again."
|
||
cleanup_build_dir
|
||
exit 1
|
||
fi
|
||
|
||
local plugin_src="${build_dir}/claude-mem/openclaw"
|
||
|
||
# Build the TypeScript plugin
|
||
info "Building TypeScript plugin..."
|
||
if ! (cd "$plugin_src" && NODE_ENV=development npm install --ignore-scripts 2>&1 && npx tsc 2>&1); then
|
||
error "Failed to build the claude-mem OpenClaw plugin"
|
||
error "Make sure Node.js and npm are installed."
|
||
cleanup_build_dir
|
||
exit 1
|
||
fi
|
||
|
||
# Create minimal installable package (matches Dockerfile.e2e pattern)
|
||
local installable_dir="${build_dir}/claude-mem-installable"
|
||
mkdir -p "${installable_dir}/dist"
|
||
|
||
cp "${plugin_src}/dist/index.js" "${installable_dir}/dist/"
|
||
cp "${plugin_src}/dist/index.d.ts" "${installable_dir}/dist/" 2>/dev/null || true
|
||
cp "${plugin_src}/openclaw.plugin.json" "${installable_dir}/"
|
||
|
||
# Generate the installable package.json with openclaw.extensions field
|
||
node -e "
|
||
const pkg = {
|
||
name: 'claude-mem',
|
||
version: '1.0.0',
|
||
type: 'module',
|
||
main: 'dist/index.js',
|
||
openclaw: { extensions: ['./dist/index.js'] }
|
||
};
|
||
require('fs').writeFileSync('${installable_dir}/package.json', JSON.stringify(pkg, null, 2));
|
||
"
|
||
|
||
# Install the plugin using OpenClaw's CLI
|
||
info "Installing claude-mem plugin into OpenClaw..."
|
||
if ! node "$OPENCLAW_PATH" plugins install "$installable_dir" 2>&1; then
|
||
error "Failed to install claude-mem plugin"
|
||
error "Try manually: node ${OPENCLAW_PATH} plugins install <path>"
|
||
cleanup_build_dir
|
||
exit 1
|
||
fi
|
||
|
||
# Enable the plugin
|
||
info "Enabling claude-mem plugin..."
|
||
if ! node "$OPENCLAW_PATH" plugins enable claude-mem 2>&1; then
|
||
error "Failed to enable claude-mem plugin"
|
||
error "Try manually: node ${OPENCLAW_PATH} plugins enable claude-mem"
|
||
cleanup_build_dir
|
||
exit 1
|
||
fi
|
||
|
||
cleanup_build_dir
|
||
trap - EXIT
|
||
success "claude-mem plugin installed and enabled"
|
||
}
|
||
|
||
###############################################################################
|
||
# Memory slot configuration
|
||
# Sets plugins.slots.memory = "claude-mem" in ~/.openclaw/openclaw.json
|
||
###############################################################################
|
||
|
||
configure_memory_slot() {
|
||
local config_dir="${HOME}/.openclaw"
|
||
local config_file="${config_dir}/openclaw.json"
|
||
|
||
mkdir -p "$config_dir"
|
||
|
||
if [[ ! -f "$config_file" ]]; then
|
||
# No config file exists — create one with the memory slot
|
||
info "Creating OpenClaw configuration with claude-mem memory slot..."
|
||
node -e "
|
||
const config = {
|
||
plugins: {
|
||
slots: { memory: 'claude-mem' },
|
||
entries: {
|
||
'claude-mem': {
|
||
enabled: true,
|
||
config: {
|
||
workerPort: 37777,
|
||
syncMemoryFile: true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2));
|
||
"
|
||
success "Created ${config_file} with memory slot set to claude-mem"
|
||
return 0
|
||
fi
|
||
|
||
# Config file exists — update it to set the memory slot
|
||
info "Updating OpenClaw configuration to use claude-mem memory slot..."
|
||
|
||
# Use node for reliable JSON manipulation
|
||
node -e "
|
||
const fs = require('fs');
|
||
const config = JSON.parse(fs.readFileSync('${config_file}', 'utf8'));
|
||
|
||
// Ensure plugins structure exists
|
||
if (!config.plugins) config.plugins = {};
|
||
if (!config.plugins.slots) config.plugins.slots = {};
|
||
if (!config.plugins.entries) config.plugins.entries = {};
|
||
|
||
// Set memory slot to claude-mem
|
||
config.plugins.slots.memory = 'claude-mem';
|
||
|
||
// Ensure claude-mem entry exists and is enabled
|
||
if (!config.plugins.entries['claude-mem']) {
|
||
config.plugins.entries['claude-mem'] = {
|
||
enabled: true,
|
||
config: {
|
||
workerPort: 37777,
|
||
syncMemoryFile: true
|
||
}
|
||
};
|
||
} else {
|
||
config.plugins.entries['claude-mem'].enabled = true;
|
||
}
|
||
|
||
fs.writeFileSync('${config_file}', JSON.stringify(config, null, 2));
|
||
"
|
||
|
||
success "Memory slot set to claude-mem in ${config_file}"
|
||
}
|
||
|
||
###############################################################################
|
||
# AI Provider setup — interactive provider selection
|
||
# Reads defaults from SettingsDefaultsManager.ts (single source of truth)
|
||
###############################################################################
|
||
|
||
AI_PROVIDER=""
|
||
AI_PROVIDER_API_KEY=""
|
||
|
||
mask_api_key() {
|
||
local key="$1"
|
||
local len=${#key}
|
||
if (( len <= 4 )); then
|
||
echo "****"
|
||
else
|
||
local masked_len=$((len - 4))
|
||
local mask=""
|
||
for (( i=0; i<masked_len; i++ )); do
|
||
mask+="*"
|
||
done
|
||
echo "${mask}${key: -4}"
|
||
fi
|
||
}
|
||
|
||
setup_ai_provider() {
|
||
echo ""
|
||
info "AI Provider Configuration"
|
||
echo ""
|
||
|
||
if [[ "$NON_INTERACTIVE" == "--non-interactive" ]] || [[ ! -t 0 ]]; then
|
||
info "Non-interactive mode: defaulting to Claude Max Plan (no API key needed)"
|
||
AI_PROVIDER="claude"
|
||
return 0
|
||
fi
|
||
|
||
echo -e " Choose your AI provider for claude-mem:"
|
||
echo ""
|
||
echo -e " ${COLOR_BOLD}1)${COLOR_RESET} Claude Max Plan ${COLOR_GREEN}(recommended)${COLOR_RESET}"
|
||
echo -e " Uses your existing subscription, no API key needed"
|
||
echo ""
|
||
echo -e " ${COLOR_BOLD}2)${COLOR_RESET} Gemini"
|
||
echo -e " Free tier available — requires API key from ai.google.dev"
|
||
echo ""
|
||
echo -e " ${COLOR_BOLD}3)${COLOR_RESET} OpenRouter"
|
||
echo -e " Pay-per-use — requires API key from openrouter.ai"
|
||
echo ""
|
||
|
||
local choice
|
||
while true; do
|
||
prompt_user "Enter choice [1/2/3] (default: 1):"
|
||
read -r choice
|
||
choice="${choice:-1}"
|
||
|
||
case "$choice" in
|
||
1)
|
||
AI_PROVIDER="claude"
|
||
success "Selected: Claude Max Plan (CLI authentication)"
|
||
break
|
||
;;
|
||
2)
|
||
AI_PROVIDER="gemini"
|
||
echo ""
|
||
prompt_user "Enter your Gemini API key (from https://ai.google.dev):"
|
||
read -rs AI_PROVIDER_API_KEY
|
||
echo ""
|
||
if [[ -z "$AI_PROVIDER_API_KEY" ]]; then
|
||
warn "No API key provided — you can add it later in ~/.claude-mem/settings.json"
|
||
else
|
||
success "Gemini API key set ($(mask_api_key "$AI_PROVIDER_API_KEY"))"
|
||
fi
|
||
break
|
||
;;
|
||
3)
|
||
AI_PROVIDER="openrouter"
|
||
echo ""
|
||
prompt_user "Enter your OpenRouter API key (from https://openrouter.ai):"
|
||
read -rs AI_PROVIDER_API_KEY
|
||
echo ""
|
||
if [[ -z "$AI_PROVIDER_API_KEY" ]]; then
|
||
warn "No API key provided — you can add it later in ~/.claude-mem/settings.json"
|
||
else
|
||
success "OpenRouter API key set ($(mask_api_key "$AI_PROVIDER_API_KEY"))"
|
||
fi
|
||
break
|
||
;;
|
||
*)
|
||
warn "Invalid choice. Please enter 1, 2, or 3."
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
###############################################################################
|
||
# Write settings.json — creates ~/.claude-mem/settings.json with all defaults
|
||
# Schema: flat key-value (not nested { env: {...} })
|
||
# Defaults sourced from SettingsDefaultsManager.ts
|
||
###############################################################################
|
||
|
||
write_settings() {
|
||
local settings_dir="${HOME}/.claude-mem"
|
||
local settings_file="${settings_dir}/settings.json"
|
||
|
||
mkdir -p "$settings_dir"
|
||
|
||
# Pass provider and API key via environment variables to avoid shell-to-JS injection
|
||
INSTALLER_AI_PROVIDER="$AI_PROVIDER" \
|
||
INSTALLER_AI_API_KEY="$AI_PROVIDER_API_KEY" \
|
||
INSTALLER_SETTINGS_FILE="$settings_file" \
|
||
node -e "
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const homedir = require('os').homedir();
|
||
const provider = process.env.INSTALLER_AI_PROVIDER;
|
||
const apiKey = process.env.INSTALLER_AI_API_KEY || '';
|
||
const settingsPath = process.env.INSTALLER_SETTINGS_FILE;
|
||
|
||
// All defaults from SettingsDefaultsManager.ts
|
||
const defaults = {
|
||
CLAUDE_MEM_MODEL: 'claude-sonnet-4-5',
|
||
CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50',
|
||
CLAUDE_MEM_WORKER_PORT: '37777',
|
||
CLAUDE_MEM_WORKER_HOST: '127.0.0.1',
|
||
CLAUDE_MEM_SKIP_TOOLS: 'ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion',
|
||
CLAUDE_MEM_PROVIDER: 'claude',
|
||
CLAUDE_MEM_CLAUDE_AUTH_METHOD: 'cli',
|
||
CLAUDE_MEM_GEMINI_API_KEY: '',
|
||
CLAUDE_MEM_GEMINI_MODEL: 'gemini-2.5-flash-lite',
|
||
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: 'true',
|
||
CLAUDE_MEM_OPENROUTER_API_KEY: '',
|
||
CLAUDE_MEM_OPENROUTER_MODEL: 'xiaomi/mimo-v2-flash:free',
|
||
CLAUDE_MEM_OPENROUTER_SITE_URL: '',
|
||
CLAUDE_MEM_OPENROUTER_APP_NAME: 'claude-mem',
|
||
CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES: '20',
|
||
CLAUDE_MEM_OPENROUTER_MAX_TOKENS: '100000',
|
||
CLAUDE_MEM_DATA_DIR: path.join(homedir, '.claude-mem'),
|
||
CLAUDE_MEM_LOG_LEVEL: 'INFO',
|
||
CLAUDE_MEM_PYTHON_VERSION: '3.13',
|
||
CLAUDE_CODE_PATH: '',
|
||
CLAUDE_MEM_MODE: 'code',
|
||
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: 'true',
|
||
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: 'true',
|
||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: 'true',
|
||
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: 'true',
|
||
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: 'bugfix,feature,refactor,discovery,decision,change',
|
||
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: 'how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off',
|
||
CLAUDE_MEM_CONTEXT_FULL_COUNT: '5',
|
||
CLAUDE_MEM_CONTEXT_FULL_FIELD: 'narrative',
|
||
CLAUDE_MEM_CONTEXT_SESSION_COUNT: '10',
|
||
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: 'true',
|
||
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
|
||
CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED: 'false',
|
||
CLAUDE_MEM_EXCLUDED_PROJECTS: '',
|
||
CLAUDE_MEM_FOLDER_MD_EXCLUDE: '[]'
|
||
};
|
||
|
||
// Build provider-specific overrides safely from environment variables
|
||
const overrides = { CLAUDE_MEM_PROVIDER: provider };
|
||
if (provider === 'claude') {
|
||
overrides.CLAUDE_MEM_CLAUDE_AUTH_METHOD = 'cli';
|
||
} else if (provider === 'gemini') {
|
||
overrides.CLAUDE_MEM_GEMINI_API_KEY = apiKey;
|
||
overrides.CLAUDE_MEM_GEMINI_MODEL = 'gemini-2.5-flash-lite';
|
||
} else if (provider === 'openrouter') {
|
||
overrides.CLAUDE_MEM_OPENROUTER_API_KEY = apiKey;
|
||
overrides.CLAUDE_MEM_OPENROUTER_MODEL = 'xiaomi/mimo-v2-flash:free';
|
||
}
|
||
|
||
const settings = Object.assign(defaults, overrides);
|
||
|
||
// If settings file already exists, merge (preserve user customizations)
|
||
if (fs.existsSync(settingsPath)) {
|
||
try {
|
||
let existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||
// Handle old nested schema
|
||
if (existing.env && typeof existing.env === 'object') {
|
||
existing = existing.env;
|
||
}
|
||
// Existing settings take priority, except for provider settings we just set
|
||
for (const key of Object.keys(existing)) {
|
||
if (!(key in overrides) && key in defaults) {
|
||
settings[key] = existing[key];
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// Corrupted file — overwrite with fresh defaults
|
||
}
|
||
}
|
||
|
||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||
"
|
||
|
||
success "Settings written to ${settings_file}"
|
||
}
|
||
|
||
###############################################################################
|
||
# Locate the installed claude-mem plugin directory
|
||
# Checks common OpenClaw and Claude Code plugin install paths
|
||
###############################################################################
|
||
|
||
CLAUDE_MEM_INSTALL_DIR=""
|
||
|
||
find_claude_mem_install_dir() {
|
||
local -a search_paths=(
|
||
"${HOME}/.openclaw/extensions/claude-mem"
|
||
"${HOME}/.claude/plugins/marketplaces/thedotmack"
|
||
"${HOME}/.openclaw/plugins/claude-mem"
|
||
)
|
||
|
||
for candidate in "${search_paths[@]}"; do
|
||
if [[ -f "${candidate}/plugin/scripts/worker-service.cjs" ]]; then
|
||
CLAUDE_MEM_INSTALL_DIR="$candidate"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
# Fallback: search for the worker script under common plugin roots
|
||
local -a roots=(
|
||
"${HOME}/.openclaw"
|
||
"${HOME}/.claude/plugins"
|
||
)
|
||
for root in "${roots[@]}"; do
|
||
if [[ -d "$root" ]]; then
|
||
local found
|
||
found="$(find "$root" -name "worker-service.cjs" -path "*/plugin/scripts/*" 2>/dev/null | head -n 1)" || true
|
||
if [[ -n "$found" ]]; then
|
||
# Strip /plugin/scripts/worker-service.cjs to get the install dir
|
||
CLAUDE_MEM_INSTALL_DIR="${found%/plugin/scripts/worker-service.cjs}"
|
||
return 0
|
||
fi
|
||
fi
|
||
done
|
||
|
||
CLAUDE_MEM_INSTALL_DIR=""
|
||
return 1
|
||
}
|
||
|
||
###############################################################################
|
||
# Worker service startup
|
||
# Starts the claude-mem worker using bun in the background
|
||
###############################################################################
|
||
|
||
WORKER_PID=""
|
||
|
||
start_worker() {
|
||
info "Starting claude-mem worker service..."
|
||
|
||
if ! find_claude_mem_install_dir; then
|
||
error "Cannot find claude-mem plugin installation directory"
|
||
error "Expected worker-service.cjs in one of:"
|
||
error " ~/.openclaw/extensions/claude-mem/plugin/scripts/"
|
||
error " ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/"
|
||
error ""
|
||
error "Try reinstalling the plugin and re-running this installer."
|
||
return 1
|
||
fi
|
||
|
||
local worker_script="${CLAUDE_MEM_INSTALL_DIR}/plugin/scripts/worker-service.cjs"
|
||
local log_dir="${HOME}/.claude-mem/logs"
|
||
local log_date
|
||
log_date="$(date +%Y-%m-%d)"
|
||
local log_file="${log_dir}/worker-${log_date}.log"
|
||
|
||
mkdir -p "$log_dir"
|
||
|
||
# Ensure bun path is available
|
||
if [[ -z "$BUN_PATH" ]]; then
|
||
if ! find_bun_path; then
|
||
error "Bun not found — cannot start worker service"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Start worker in background with nohup
|
||
CLAUDE_MEM_WORKER_PORT=37777 nohup "$BUN_PATH" "$worker_script" \
|
||
>> "$log_file" 2>&1 &
|
||
WORKER_PID=$!
|
||
|
||
# Write PID file for future management
|
||
local pid_file="${HOME}/.claude-mem/worker.pid"
|
||
mkdir -p "${HOME}/.claude-mem"
|
||
node -e "
|
||
const info = {
|
||
pid: ${WORKER_PID},
|
||
port: 37777,
|
||
startedAt: new Date().toISOString(),
|
||
version: 'installer'
|
||
};
|
||
require('fs').writeFileSync('${pid_file}', JSON.stringify(info, null, 2));
|
||
"
|
||
|
||
success "Worker process started (PID: ${WORKER_PID})"
|
||
info "Logs: ${log_file}"
|
||
}
|
||
|
||
###############################################################################
|
||
# Health verification
|
||
# Polls http://localhost:37777/api/health up to 10 times with 1-second intervals
|
||
###############################################################################
|
||
|
||
verify_health() {
|
||
local max_attempts=10
|
||
local attempt=1
|
||
local health_url="http://127.0.0.1:37777/api/health"
|
||
|
||
info "Verifying worker health..."
|
||
|
||
while (( attempt <= max_attempts )); do
|
||
local response
|
||
response="$(curl -s -o /dev/null -w "%{http_code}" "$health_url" 2>/dev/null)" || true
|
||
|
||
if [[ "$response" == "200" ]]; then
|
||
# Verify the response body contains status:ok
|
||
local body
|
||
body="$(curl -s "$health_url" 2>/dev/null)" || true
|
||
if echo "$body" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"'; then
|
||
success "Worker is healthy (port 37777)"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
if (( attempt < max_attempts )); then
|
||
info "Waiting for worker to start... (attempt ${attempt}/${max_attempts})"
|
||
fi
|
||
sleep 1
|
||
attempt=$((attempt + 1))
|
||
done
|
||
|
||
warn "Worker health check timed out after ${max_attempts} attempts"
|
||
warn "The worker may still be starting up. Check status with:"
|
||
warn " curl http://127.0.0.1:37777/api/health"
|
||
warn " Or check logs: ~/.claude-mem/logs/"
|
||
return 1
|
||
}
|
||
|
||
###############################################################################
|
||
# Observation feed setup — optional interactive channel configuration
|
||
###############################################################################
|
||
|
||
FEED_CHANNEL=""
|
||
FEED_TARGET_ID=""
|
||
FEED_CONFIGURED=false
|
||
|
||
setup_observation_feed() {
|
||
echo ""
|
||
echo -e " ${COLOR_BOLD}Real-Time Observation Feed${COLOR_RESET}"
|
||
echo ""
|
||
echo " claude-mem can stream AI-compressed observations to a messaging"
|
||
echo " channel in real time. Every time an agent learns something,"
|
||
echo " you'll see it in your chat."
|
||
echo ""
|
||
|
||
if [[ "$NON_INTERACTIVE" == "--non-interactive" ]] || [[ ! -t 0 ]]; then
|
||
info "Non-interactive mode: skipping observation feed setup"
|
||
info "Configure later in ~/.openclaw/openclaw.json under"
|
||
info " plugins.entries.claude-mem.config.observationFeed"
|
||
return 0
|
||
fi
|
||
|
||
prompt_user "Would you like to set up real-time observation streaming to a messaging channel? (y/n)"
|
||
local answer
|
||
read -r answer
|
||
answer="${answer:-n}"
|
||
|
||
if [[ "${answer,,}" != "y" && "${answer,,}" != "yes" ]]; then
|
||
echo ""
|
||
info "Skipped observation feed setup."
|
||
info "You can configure it later by re-running this installer or"
|
||
info "editing ~/.openclaw/openclaw.json under"
|
||
info " plugins.entries.claude-mem.config.observationFeed"
|
||
return 0
|
||
fi
|
||
|
||
echo ""
|
||
echo -e " ${COLOR_BOLD}Select your messaging channel:${COLOR_RESET}"
|
||
echo ""
|
||
echo -e " ${COLOR_BOLD}1)${COLOR_RESET} Telegram"
|
||
echo -e " ${COLOR_BOLD}2)${COLOR_RESET} Discord"
|
||
echo -e " ${COLOR_BOLD}3)${COLOR_RESET} Slack"
|
||
echo -e " ${COLOR_BOLD}4)${COLOR_RESET} Signal"
|
||
echo -e " ${COLOR_BOLD}5)${COLOR_RESET} WhatsApp"
|
||
echo -e " ${COLOR_BOLD}6)${COLOR_RESET} LINE"
|
||
echo ""
|
||
|
||
local channel_choice
|
||
while true; do
|
||
prompt_user "Enter choice [1-6]:"
|
||
read -r channel_choice
|
||
|
||
case "$channel_choice" in
|
||
1)
|
||
FEED_CHANNEL="telegram"
|
||
echo ""
|
||
echo -e " ${COLOR_CYAN}How to find your Telegram chat ID:${COLOR_RESET}"
|
||
echo " Message @userinfobot on Telegram (https://t.me/userinfobot)"
|
||
echo " — it replies with your numeric chat ID."
|
||
echo " For groups, the ID is negative (e.g., -1001234567890)."
|
||
break
|
||
;;
|
||
2)
|
||
FEED_CHANNEL="discord"
|
||
echo ""
|
||
echo -e " ${COLOR_CYAN}How to find your Discord channel ID:${COLOR_RESET}"
|
||
echo " Enable Developer Mode (Settings → Advanced → Developer Mode),"
|
||
echo " right-click the target channel → Copy Channel ID"
|
||
break
|
||
;;
|
||
3)
|
||
FEED_CHANNEL="slack"
|
||
echo ""
|
||
echo -e " ${COLOR_CYAN}How to find your Slack channel ID:${COLOR_RESET}"
|
||
echo " Open the channel, click the channel name at top,"
|
||
echo " scroll to bottom — ID looks like C01ABC2DEFG"
|
||
break
|
||
;;
|
||
4)
|
||
FEED_CHANNEL="signal"
|
||
echo ""
|
||
echo -e " ${COLOR_CYAN}How to find your Signal target ID:${COLOR_RESET}"
|
||
echo " Use the phone number or group ID from your"
|
||
echo " OpenClaw Signal plugin config"
|
||
break
|
||
;;
|
||
5)
|
||
FEED_CHANNEL="whatsapp"
|
||
echo ""
|
||
echo -e " ${COLOR_CYAN}How to find your WhatsApp target ID:${COLOR_RESET}"
|
||
echo " Use the phone number or group JID from your"
|
||
echo " OpenClaw WhatsApp plugin config"
|
||
break
|
||
;;
|
||
6)
|
||
FEED_CHANNEL="line"
|
||
echo ""
|
||
echo -e " ${COLOR_CYAN}How to find your LINE target ID:${COLOR_RESET}"
|
||
echo " Use the user ID or group ID from the"
|
||
echo " LINE Developer Console"
|
||
break
|
||
;;
|
||
*)
|
||
warn "Invalid choice. Please enter a number between 1 and 6."
|
||
;;
|
||
esac
|
||
done
|
||
|
||
echo ""
|
||
prompt_user "Enter your ${FEED_CHANNEL} target ID:"
|
||
read -r FEED_TARGET_ID
|
||
|
||
if [[ -z "$FEED_TARGET_ID" ]]; then
|
||
warn "No target ID provided — skipping observation feed setup."
|
||
warn "You can configure it later in ~/.openclaw/openclaw.json"
|
||
FEED_CHANNEL=""
|
||
return 0
|
||
fi
|
||
|
||
success "Observation feed: ${FEED_CHANNEL} → ${FEED_TARGET_ID}"
|
||
FEED_CONFIGURED=true
|
||
}
|
||
|
||
###############################################################################
|
||
# Write observation feed config into ~/.openclaw/openclaw.json
|
||
###############################################################################
|
||
|
||
write_observation_feed_config() {
|
||
if [[ "$FEED_CONFIGURED" != "true" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
local config_file="${HOME}/.openclaw/openclaw.json"
|
||
|
||
if [[ ! -f "$config_file" ]]; then
|
||
warn "OpenClaw config file not found at ${config_file}"
|
||
warn "Cannot write observation feed config."
|
||
return 1
|
||
fi
|
||
|
||
info "Writing observation feed configuration..."
|
||
|
||
# Use jq if available, fall back to python3, then node for JSON manipulation
|
||
if command -v jq &>/dev/null; then
|
||
local tmp_file
|
||
tmp_file="$(mktemp)"
|
||
jq --arg channel "$FEED_CHANNEL" --arg target "$FEED_TARGET_ID" '
|
||
.plugins //= {} |
|
||
.plugins.entries //= {} |
|
||
.plugins.entries["claude-mem"] //= {"enabled": true, "config": {}} |
|
||
.plugins.entries["claude-mem"].config //= {} |
|
||
.plugins.entries["claude-mem"].config.observationFeed = {
|
||
"enabled": true,
|
||
"channel": $channel,
|
||
"to": $target
|
||
}
|
||
' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file"
|
||
elif command -v python3 &>/dev/null; then
|
||
INSTALLER_FEED_CHANNEL="$FEED_CHANNEL" \
|
||
INSTALLER_FEED_TARGET_ID="$FEED_TARGET_ID" \
|
||
INSTALLER_CONFIG_FILE="$config_file" \
|
||
python3 -c "
|
||
import json, os
|
||
config_path = os.environ['INSTALLER_CONFIG_FILE']
|
||
channel = os.environ['INSTALLER_FEED_CHANNEL']
|
||
target_id = os.environ['INSTALLER_FEED_TARGET_ID']
|
||
|
||
with open(config_path) as f:
|
||
config = json.load(f)
|
||
|
||
config.setdefault('plugins', {})
|
||
config['plugins'].setdefault('entries', {})
|
||
config['plugins']['entries'].setdefault('claude-mem', {'enabled': True, 'config': {}})
|
||
config['plugins']['entries']['claude-mem'].setdefault('config', {})
|
||
config['plugins']['entries']['claude-mem']['config']['observationFeed'] = {
|
||
'enabled': True,
|
||
'channel': channel,
|
||
'to': target_id
|
||
}
|
||
|
||
with open(config_path, 'w') as f:
|
||
json.dump(config, f, indent=2)
|
||
"
|
||
else
|
||
# Fallback to node (always available since it's a dependency)
|
||
INSTALLER_FEED_CHANNEL="$FEED_CHANNEL" \
|
||
INSTALLER_FEED_TARGET_ID="$FEED_TARGET_ID" \
|
||
INSTALLER_CONFIG_FILE="$config_file" \
|
||
node -e "
|
||
const fs = require('fs');
|
||
const configPath = process.env.INSTALLER_CONFIG_FILE;
|
||
const channel = process.env.INSTALLER_FEED_CHANNEL;
|
||
const targetId = process.env.INSTALLER_FEED_TARGET_ID;
|
||
|
||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||
|
||
if (!config.plugins) config.plugins = {};
|
||
if (!config.plugins.entries) config.plugins.entries = {};
|
||
if (!config.plugins.entries['claude-mem']) {
|
||
config.plugins.entries['claude-mem'] = { enabled: true, config: {} };
|
||
}
|
||
if (!config.plugins.entries['claude-mem'].config) {
|
||
config.plugins.entries['claude-mem'].config = {};
|
||
}
|
||
|
||
config.plugins.entries['claude-mem'].config.observationFeed = {
|
||
enabled: true,
|
||
channel: channel,
|
||
to: targetId
|
||
};
|
||
|
||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||
"
|
||
fi
|
||
|
||
success "Observation feed config written to ${config_file}"
|
||
echo ""
|
||
echo -e " ${COLOR_BOLD}Observation feed summary:${COLOR_RESET}"
|
||
echo -e " Channel: ${COLOR_CYAN}${FEED_CHANNEL}${COLOR_RESET}"
|
||
echo -e " Target: ${COLOR_CYAN}${FEED_TARGET_ID}${COLOR_RESET}"
|
||
echo -e " Enabled: ${COLOR_GREEN}yes${COLOR_RESET}"
|
||
echo ""
|
||
info "Restart your OpenClaw gateway to activate the observation feed."
|
||
info "You should see these log lines:"
|
||
echo " [claude-mem] Observation feed starting — channel: ${FEED_CHANNEL}, target: ${FEED_TARGET_ID}"
|
||
echo ""
|
||
info "After restarting, run /claude-mem-feed in any OpenClaw chat to verify"
|
||
info "the feed is connected."
|
||
}
|
||
|
||
###############################################################################
|
||
# Completion summary
|
||
###############################################################################
|
||
|
||
print_completion_summary() {
|
||
local provider_display=""
|
||
case "$AI_PROVIDER" in
|
||
claude) provider_display="Claude Max Plan (CLI authentication)" ;;
|
||
gemini) provider_display="Gemini (gemini-2.5-flash-lite)" ;;
|
||
openrouter) provider_display="OpenRouter (xiaomi/mimo-v2-flash:free)" ;;
|
||
*) provider_display="$AI_PROVIDER" ;;
|
||
esac
|
||
|
||
echo ""
|
||
echo -e "${COLOR_MAGENTA}${COLOR_BOLD}"
|
||
echo " ┌──────────────────────────────────────────┐"
|
||
echo " │ Installation Complete! │"
|
||
echo " └──────────────────────────────────────────┘"
|
||
echo -e "${COLOR_RESET}"
|
||
|
||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Dependencies installed (Bun, uv)"
|
||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} OpenClaw gateway detected"
|
||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} claude-mem plugin installed and enabled"
|
||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Memory slot configured"
|
||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} AI provider: ${COLOR_BOLD}${provider_display}${COLOR_RESET}"
|
||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Settings written to ~/.claude-mem/settings.json"
|
||
|
||
if [[ -n "$WORKER_PID" ]] && kill -0 "$WORKER_PID" 2>/dev/null; then
|
||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Worker running on port ${COLOR_BOLD}37777${COLOR_RESET} (PID: ${WORKER_PID})"
|
||
else
|
||
echo -e " ${COLOR_YELLOW}⚠${COLOR_RESET} Worker may not be running — check logs at ~/.claude-mem/logs/"
|
||
fi
|
||
|
||
if [[ "$FEED_CONFIGURED" == "true" ]]; then
|
||
echo -e " ${COLOR_GREEN}✓${COLOR_RESET} Observation feed: ${COLOR_BOLD}${FEED_CHANNEL}${COLOR_RESET} → ${FEED_TARGET_ID}"
|
||
else
|
||
echo -e " ${COLOR_YELLOW}─${COLOR_RESET} Observation feed: not configured (optional)"
|
||
echo -e " Configure later in ~/.openclaw/openclaw.json under"
|
||
echo -e " plugins.entries.claude-mem.config.observationFeed"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e " ${COLOR_BOLD}What's next?${COLOR_RESET}"
|
||
echo ""
|
||
echo -e " ${COLOR_CYAN}1.${COLOR_RESET} Restart your OpenClaw gateway to load the plugin"
|
||
echo -e " ${COLOR_CYAN}2.${COLOR_RESET} Verify with ${COLOR_BOLD}/claude-mem-status${COLOR_RESET} in any OpenClaw chat"
|
||
echo -e " ${COLOR_CYAN}3.${COLOR_RESET} Check the viewer UI at ${COLOR_BOLD}http://localhost:37777${COLOR_RESET}"
|
||
if [[ "$FEED_CONFIGURED" == "true" ]]; then
|
||
echo -e " ${COLOR_CYAN}4.${COLOR_RESET} Run ${COLOR_BOLD}/claude-mem-feed${COLOR_RESET} to check feed status"
|
||
fi
|
||
echo ""
|
||
echo -e " ${COLOR_BOLD}To re-run this installer:${COLOR_RESET}"
|
||
echo " bash <(curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh)"
|
||
echo ""
|
||
}
|
||
|
||
###############################################################################
|
||
# Main
|
||
###############################################################################
|
||
|
||
main() {
|
||
print_banner
|
||
detect_platform
|
||
|
||
# --- Step 1: Dependencies ---
|
||
echo ""
|
||
info "${COLOR_BOLD}[1/8]${COLOR_RESET} Checking dependencies..."
|
||
echo ""
|
||
|
||
if ! check_bun; then
|
||
install_bun
|
||
fi
|
||
|
||
if ! check_uv; then
|
||
install_uv
|
||
fi
|
||
|
||
echo ""
|
||
success "All dependencies satisfied"
|
||
|
||
# --- Step 2: OpenClaw gateway ---
|
||
echo ""
|
||
info "${COLOR_BOLD}[2/8]${COLOR_RESET} Locating OpenClaw gateway..."
|
||
check_openclaw
|
||
|
||
# --- Step 3: Plugin installation ---
|
||
echo ""
|
||
info "${COLOR_BOLD}[3/8]${COLOR_RESET} Installing claude-mem plugin..."
|
||
install_plugin
|
||
|
||
# --- Step 4: Memory slot configuration ---
|
||
echo ""
|
||
info "${COLOR_BOLD}[4/8]${COLOR_RESET} Configuring memory slot..."
|
||
configure_memory_slot
|
||
|
||
# --- Step 5: AI provider setup ---
|
||
echo ""
|
||
info "${COLOR_BOLD}[5/8]${COLOR_RESET} AI provider setup..."
|
||
setup_ai_provider
|
||
|
||
# --- Step 6: Write settings ---
|
||
echo ""
|
||
info "${COLOR_BOLD}[6/8]${COLOR_RESET} Writing settings..."
|
||
write_settings
|
||
|
||
# --- Step 7: Start worker and verify ---
|
||
echo ""
|
||
info "${COLOR_BOLD}[7/8]${COLOR_RESET} Starting worker service..."
|
||
if start_worker; then
|
||
verify_health || true
|
||
else
|
||
warn "Worker startup failed — you can start it manually later"
|
||
warn " cd ~/.claude/plugins/marketplaces/thedotmack && bun plugin/scripts/worker-service.cjs"
|
||
fi
|
||
|
||
# --- Step 8: Observation feed setup (optional) ---
|
||
echo ""
|
||
info "${COLOR_BOLD}[8/8]${COLOR_RESET} Observation feed setup..."
|
||
setup_observation_feed
|
||
write_observation_feed_config
|
||
|
||
# --- Completion ---
|
||
print_completion_summary
|
||
}
|
||
|
||
main "$@"
|