From 1f834863a7d8dc80c76b7c5cdeee3790656a26d8 Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Wed, 11 Feb 2026 21:31:15 -0500 Subject: [PATCH] MAESTRO: add OpenClaw gateway detection, plugin install, and memory slot config to install.sh Add find_openclaw()/check_openclaw() for gateway detection across PATH, ~/.openclaw/, /usr/local/bin/, and node_modules paths. Add install_plugin() that clones, builds, creates installable package, and runs plugins install/enable following the Dockerfile.e2e flow. Add configure_memory_slot() that creates or updates ~/.openclaw/openclaw.json with plugins.slots.memory="claude-mem" while preserving existing config. Includes test-install.sh with 23 passing tests. Co-Authored-By: Claude Opus 4.6 --- openclaw/install.sh | 226 ++++++++++++++++++++++++++- openclaw/test-install.sh | 329 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 553 insertions(+), 2 deletions(-) create mode 100755 openclaw/test-install.sh diff --git a/openclaw/install.sh b/openclaw/install.sh index e2013be8..550f7ab1 100755 --- a/openclaw/install.sh +++ b/openclaw/install.sh @@ -267,6 +267,211 @@ install_uv() { 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 " + 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}" +} + ############################################################################### # Main ############################################################################### @@ -275,7 +480,7 @@ main() { print_banner detect_platform - # --- Step 1: Bun --- + # --- Step 1: Dependencies --- echo "" info "Checking dependencies..." echo "" @@ -284,13 +489,30 @@ main() { install_bun fi - # --- Step 2: uv --- if ! check_uv; then install_uv fi echo "" success "All dependencies satisfied" + + # --- Step 2: OpenClaw gateway --- + echo "" + info "Locating OpenClaw gateway..." + check_openclaw + + # --- Step 3: Plugin installation --- + echo "" + info "Installing claude-mem plugin..." + install_plugin + + # --- Step 4: Memory slot configuration --- + echo "" + info "Configuring memory slot..." + configure_memory_slot + + echo "" + success "OpenClaw gateway detection and plugin installation complete" } main "$@" diff --git a/openclaw/test-install.sh b/openclaw/test-install.sh new file mode 100755 index 00000000..c70e4059 --- /dev/null +++ b/openclaw/test-install.sh @@ -0,0 +1,329 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Test suite for openclaw/install.sh functions +# Tests the OpenClaw gateway detection, plugin install, and memory slot config. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_SCRIPT="${SCRIPT_DIR}/install.sh" + +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 + +############################################################################### +# Test helpers +############################################################################### + +test_pass() { + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_PASSED=$((TESTS_PASSED + 1)) + echo -e "\033[0;32m✓\033[0m $1" +} + +test_fail() { + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_FAILED=$((TESTS_FAILED + 1)) + echo -e "\033[0;31m✗\033[0m $1" + if [[ -n "${2:-}" ]]; then + echo " Detail: $2" + fi +} + +assert_eq() { + local expected="$1" actual="$2" msg="$3" + if [[ "$expected" == "$actual" ]]; then + test_pass "$msg" + else + test_fail "$msg" "expected='${expected}' actual='${actual}'" + fi +} + +assert_contains() { + local haystack="$1" needle="$2" msg="$3" + if [[ "$haystack" == *"$needle"* ]]; then + test_pass "$msg" + else + test_fail "$msg" "expected string to contain '${needle}'" + fi +} + +assert_file_exists() { + local filepath="$1" msg="$2" + if [[ -f "$filepath" ]]; then + test_pass "$msg" + else + test_fail "$msg" "file not found: ${filepath}" + fi +} + +############################################################################### +# Source the install script without running main() +# We override main to be a no-op, then source the file. +############################################################################### + +source_install_functions() { + # Create a temp file that overrides main and sources the install script + local tmp_source + tmp_source="$(mktemp)" + # Extract everything except the final `main "$@"` invocation + sed '$ d' "$INSTALL_SCRIPT" > "$tmp_source" + # Override main to prevent execution + echo 'main() { :; }' >> "$tmp_source" + # Source it (suppress color output for cleaner tests) + TERM=dumb source "$tmp_source" + rm -f "$tmp_source" +} + +source_install_functions + +############################################################################### +# Test: find_openclaw() — not found scenario +############################################################################### + +echo "" +echo "=== find_openclaw() ===" + +# Save original PATH and test with empty locations +ORIGINAL_PATH="$PATH" +ORIGINAL_HOME="$HOME" + +test_find_openclaw_not_found() { + # Use a fake HOME where nothing exists + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + PATH="/nonexistent" + OPENCLAW_PATH="" + + if find_openclaw 2>/dev/null; then + test_fail "find_openclaw should return 1 when openclaw.mjs is not found" + else + test_pass "find_openclaw returns 1 when not found" + fi + + assert_eq "" "$OPENCLAW_PATH" "OPENCLAW_PATH is empty when not found" + + HOME="$ORIGINAL_HOME" + PATH="$ORIGINAL_PATH" + rm -rf "$fake_home" +} + +test_find_openclaw_not_found + +# Test: find_openclaw() — found in HOME/.openclaw/ +test_find_openclaw_in_home() { + local fake_home + fake_home="$(mktemp -d)" + mkdir -p "${fake_home}/.openclaw" + touch "${fake_home}/.openclaw/openclaw.mjs" + + HOME="$fake_home" + PATH="/nonexistent" + OPENCLAW_PATH="" + + if find_openclaw 2>/dev/null; then + test_pass "find_openclaw finds openclaw.mjs in ~/.openclaw/" + assert_eq "${fake_home}/.openclaw/openclaw.mjs" "$OPENCLAW_PATH" "OPENCLAW_PATH set correctly" + else + test_fail "find_openclaw should find openclaw.mjs in ~/.openclaw/" + fi + + HOME="$ORIGINAL_HOME" + PATH="$ORIGINAL_PATH" + rm -rf "$fake_home" +} + +test_find_openclaw_in_home + +############################################################################### +# Test: configure_memory_slot() — creates new config +############################################################################### + +echo "" +echo "=== configure_memory_slot() ===" + +test_configure_new_config() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + configure_memory_slot >/dev/null 2>&1 + + local config_file="${fake_home}/.openclaw/openclaw.json" + assert_file_exists "$config_file" "Config file created at ~/.openclaw/openclaw.json" + + # Verify JSON structure + local memory_slot + memory_slot="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.slots.memory);")" + assert_eq "claude-mem" "$memory_slot" "Memory slot set to claude-mem in new config" + + local enabled + enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")" + assert_eq "true" "$enabled" "claude-mem entry is enabled in new config" + + local worker_port + worker_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")" + assert_eq "37777" "$worker_port" "Worker port is 37777 in new config" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_configure_new_config + +# Test: configure_memory_slot() — updates existing config +test_configure_existing_config() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + # Create an existing config with other settings + mkdir -p "${fake_home}/.openclaw" + local config_file="${fake_home}/.openclaw/openclaw.json" + node -e " + const config = { + gateway: { mode: 'local' }, + plugins: { + slots: { memory: 'memory-core' }, + entries: { + 'some-other-plugin': { enabled: true } + } + } + }; + require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2)); + " + + configure_memory_slot >/dev/null 2>&1 + + # Verify memory slot was updated + local memory_slot + memory_slot="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.slots.memory);")" + assert_eq "claude-mem" "$memory_slot" "Memory slot updated from memory-core to claude-mem" + + # Verify existing settings preserved + local gateway_mode + gateway_mode="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.gateway.mode);")" + assert_eq "local" "$gateway_mode" "Existing gateway.mode setting preserved" + + # Verify other plugin still present + local other_plugin + other_plugin="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['some-other-plugin'].enabled);")" + assert_eq "true" "$other_plugin" "Existing plugin entries preserved" + + # Verify claude-mem entry was added + local cm_enabled + cm_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")" + assert_eq "true" "$cm_enabled" "claude-mem entry added and enabled" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_configure_existing_config + +# Test: configure_memory_slot() — preserves existing claude-mem config +test_configure_preserves_existing_cm_config() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + mkdir -p "${fake_home}/.openclaw" + local config_file="${fake_home}/.openclaw/openclaw.json" + node -e " + const config = { + plugins: { + slots: { memory: 'memory-core' }, + entries: { + 'claude-mem': { + enabled: false, + config: { + workerPort: 38888, + observationFeed: { enabled: true, channel: 'telegram', to: '12345' } + } + } + } + } + }; + require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2)); + " + + configure_memory_slot >/dev/null 2>&1 + + # Should enable it but preserve existing config + local cm_enabled + cm_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")" + assert_eq "true" "$cm_enabled" "claude-mem entry enabled when previously disabled" + + local custom_port + custom_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")" + assert_eq "38888" "$custom_port" "Existing custom workerPort preserved" + + local feed_channel + feed_channel="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.channel);")" + assert_eq "telegram" "$feed_channel" "Existing observationFeed config preserved" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_configure_preserves_existing_cm_config + +############################################################################### +# Test: version_gte() — already exists from phase 1 +############################################################################### + +echo "" +echo "=== version_gte() ===" + +if version_gte "1.2.0" "1.1.14"; then + test_pass "version_gte: 1.2.0 >= 1.1.14" +else + test_fail "version_gte: 1.2.0 >= 1.1.14" +fi + +if version_gte "1.1.14" "1.1.14"; then + test_pass "version_gte: 1.1.14 >= 1.1.14 (equal)" +else + test_fail "version_gte: 1.1.14 >= 1.1.14 (equal)" +fi + +if ! version_gte "1.0.0" "1.1.14"; then + test_pass "version_gte: 1.0.0 < 1.1.14" +else + test_fail "version_gte: 1.0.0 < 1.1.14" +fi + +############################################################################### +# Test: Script structure validation +############################################################################### + +echo "" +echo "=== Script structure ===" + +# Verify all required functions exist +for fn in find_openclaw check_openclaw install_plugin configure_memory_slot; do + if declare -f "$fn" &>/dev/null; then + test_pass "Function ${fn}() is defined" + else + test_fail "Function ${fn}() should be defined" + fi +done + +# Verify the CLAUDE_MEM_REPO constant +assert_contains "$CLAUDE_MEM_REPO" "github.com/thedotmack/claude-mem" "CLAUDE_MEM_REPO points to correct repository" + +############################################################################### +# Summary +############################################################################### + +echo "" +echo "========================================" +echo "Results: ${TESTS_PASSED}/${TESTS_RUN} passed, ${TESTS_FAILED} failed" +echo "========================================" + +if [[ "$TESTS_FAILED" -gt 0 ]]; then + exit 1 +fi + +exit 0