From 6f35e543ca9582ccad77977007ee13227c7576e8 Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Wed, 11 Feb 2026 21:55:24 -0500 Subject: [PATCH] MAESTRO: add observation feed interactive setup, config writer, and updated completion summary to install.sh Co-Authored-By: Claude Opus 4.6 --- openclaw/install.sh | 235 ++++++++++++++++++++++++++++++--- openclaw/test-install.sh | 277 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 487 insertions(+), 25 deletions(-) diff --git a/openclaw/install.sh b/openclaw/install.sh index fe964903..840dba3c 100755 --- a/openclaw/install.sh +++ b/openclaw/install.sh @@ -805,6 +805,197 @@ verify_health() { 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..." + + # Pass values via environment variables to avoid injection + 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')); + + // Ensure nested structure exists + 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 = {}; + } + + // Set observation feed config + config.plugins.entries['claude-mem'].config.observationFeed = { + enabled: true, + channel: channel, + to: targetId + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + " + + 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 ############################################################################### @@ -838,17 +1029,23 @@ print_completion_summary() { 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}Next step: Set up your observation feed${COLOR_RESET}" + echo -e " ${COLOR_BOLD}What's next?${COLOR_RESET}" echo "" - echo " claude-mem can send AI-compressed observations to your preferred" - echo " messaging channel. Supported channels:" - echo "" - echo -e " ${COLOR_CYAN}•${COLOR_RESET} Telegram ${COLOR_CYAN}•${COLOR_RESET} Discord ${COLOR_CYAN}•${COLOR_RESET} Slack" - echo -e " ${COLOR_CYAN}•${COLOR_RESET} Signal ${COLOR_CYAN}•${COLOR_RESET} WhatsApp ${COLOR_CYAN}•${COLOR_RESET} LINE" - echo "" - echo " Configure in ~/.openclaw/openclaw.json under" - echo " plugins.entries.claude-mem.config.observationFeed" + 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)" @@ -865,7 +1062,7 @@ main() { # --- Step 1: Dependencies --- echo "" - info "${COLOR_BOLD}[1/7]${COLOR_RESET} Checking dependencies..." + info "${COLOR_BOLD}[1/8]${COLOR_RESET} Checking dependencies..." echo "" if ! check_bun; then @@ -881,32 +1078,32 @@ main() { # --- Step 2: OpenClaw gateway --- echo "" - info "${COLOR_BOLD}[2/7]${COLOR_RESET} Locating OpenClaw gateway..." + info "${COLOR_BOLD}[2/8]${COLOR_RESET} Locating OpenClaw gateway..." check_openclaw # --- Step 3: Plugin installation --- echo "" - info "${COLOR_BOLD}[3/7]${COLOR_RESET} Installing claude-mem plugin..." + info "${COLOR_BOLD}[3/8]${COLOR_RESET} Installing claude-mem plugin..." install_plugin # --- Step 4: Memory slot configuration --- echo "" - info "${COLOR_BOLD}[4/7]${COLOR_RESET} Configuring memory slot..." + info "${COLOR_BOLD}[4/8]${COLOR_RESET} Configuring memory slot..." configure_memory_slot # --- Step 5: AI provider setup --- echo "" - info "${COLOR_BOLD}[5/7]${COLOR_RESET} AI provider setup..." + info "${COLOR_BOLD}[5/8]${COLOR_RESET} AI provider setup..." setup_ai_provider # --- Step 6: Write settings --- echo "" - info "${COLOR_BOLD}[6/7]${COLOR_RESET} Writing settings..." + info "${COLOR_BOLD}[6/8]${COLOR_RESET} Writing settings..." write_settings # --- Step 7: Start worker and verify --- echo "" - info "${COLOR_BOLD}[7/7]${COLOR_RESET} Starting worker service..." + info "${COLOR_BOLD}[7/8]${COLOR_RESET} Starting worker service..." if start_worker; then verify_health || true else @@ -914,6 +1111,12 @@ main() { 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 } diff --git a/openclaw/test-install.sh b/openclaw/test-install.sh index 1d2442ec..e359a447 100755 --- a/openclaw/test-install.sh +++ b/openclaw/test-install.sh @@ -700,18 +700,19 @@ echo "=== print_completion_summary() ===" test_print_completion_summary() { AI_PROVIDER="claude" WORKER_PID="" + FEED_CONFIGURED=false + FEED_CHANNEL="" + FEED_TARGET_ID="" local output output="$(print_completion_summary 2>&1)" assert_contains "$output" "Installation Complete" "Completion summary shows 'Installation Complete'" assert_contains "$output" "Claude Max Plan" "Completion summary shows correct provider" - assert_contains "$output" "Telegram" "Completion summary mentions Telegram channel" - assert_contains "$output" "Discord" "Completion summary mentions Discord channel" - assert_contains "$output" "Slack" "Completion summary mentions Slack channel" - assert_contains "$output" "Signal" "Completion summary mentions Signal channel" - assert_contains "$output" "WhatsApp" "Completion summary mentions WhatsApp channel" - assert_contains "$output" "LINE" "Completion summary mentions LINE channel" + assert_contains "$output" "not configured" "Completion summary shows feed 'not configured' when skipped" + assert_contains "$output" "What's next" "Completion summary shows What's next section" + assert_contains "$output" "/claude-mem-status" "Completion summary mentions status command" + assert_contains "$output" "localhost:37777" "Completion summary mentions viewer URL" assert_contains "$output" "re-run this installer" "Completion summary shows re-run instructions" } @@ -720,6 +721,7 @@ test_print_completion_summary test_print_completion_summary_gemini() { AI_PROVIDER="gemini" WORKER_PID="" + FEED_CONFIGURED=false local output output="$(print_completion_summary 2>&1)" @@ -732,6 +734,7 @@ test_print_completion_summary_gemini test_print_completion_summary_openrouter() { AI_PROVIDER="openrouter" WORKER_PID="" + FEED_CONFIGURED=false local output output="$(print_completion_summary 2>&1)" @@ -795,15 +798,271 @@ test_main_calls_completion_summary() { test_main_calls_completion_summary test_main_has_progress_indicators() { - if grep -q '\[1/7\]' "$INSTALL_SCRIPT" && grep -q '\[7/7\]' "$INSTALL_SCRIPT"; then - test_pass "main() has progress indicators [1/7] through [7/7]" + if grep -q '\[1/8\]' "$INSTALL_SCRIPT" && grep -q '\[8/8\]' "$INSTALL_SCRIPT"; then + test_pass "main() has progress indicators [1/8] through [8/8]" else - test_fail "main() should have progress indicators [1/7] through [7/7]" + test_fail "main() should have progress indicators [1/8] through [8/8]" fi } test_main_has_progress_indicators +test_main_calls_setup_observation_feed() { + if grep -q 'setup_observation_feed' "$INSTALL_SCRIPT"; then + test_pass "main() calls setup_observation_feed" + else + test_fail "main() should call setup_observation_feed" + fi +} + +test_main_calls_setup_observation_feed + +test_main_calls_write_observation_feed_config() { + if grep -q 'write_observation_feed_config' "$INSTALL_SCRIPT"; then + test_pass "main() calls write_observation_feed_config" + else + test_fail "main() should call write_observation_feed_config" + fi +} + +test_main_calls_write_observation_feed_config + +############################################################################### +# Test: setup_observation_feed() — function exists and non-interactive skips +############################################################################### + +echo "" +echo "=== setup_observation_feed() ===" + +for fn in setup_observation_feed write_observation_feed_config; do + if declare -f "$fn" &>/dev/null; then + test_pass "Function ${fn}() is defined" + else + test_fail "Function ${fn}() should be defined" + fi +done + +test_setup_observation_feed_non_interactive() { + # Non-interactive mode should skip feed setup without error + local feed_result + feed_result="$(bash -c ' + set -euo pipefail + TERM=dumb + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + set -- "--non-interactive" + source "$tmp" + rm -f "$tmp" + setup_observation_feed 2>/dev/null + echo "CHANNEL=$FEED_CHANNEL" + echo "CONFIGURED=$FEED_CONFIGURED" + ' 2>/dev/null)" || true + + assert_contains "$feed_result" "CHANNEL=" "Non-interactive mode: FEED_CHANNEL is empty" + assert_contains "$feed_result" "CONFIGURED=false" "Non-interactive mode: FEED_CONFIGURED is false" +} + +test_setup_observation_feed_non_interactive + +############################################################################### +# Test: write_observation_feed_config() — writes correct JSON structure +############################################################################### + +echo "" +echo "=== write_observation_feed_config() ===" + +test_write_observation_feed_config_writes_json() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + # Create an existing openclaw.json with claude-mem entry + mkdir -p "${fake_home}/.openclaw" + local config_file="${fake_home}/.openclaw/openclaw.json" + 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)); + " + + FEED_CHANNEL="telegram" + FEED_TARGET_ID="123456789" + FEED_CONFIGURED="true" + + write_observation_feed_config >/dev/null 2>&1 + + # Verify observationFeed was written + local feed_enabled + feed_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.enabled);")" + assert_eq "true" "$feed_enabled" "observationFeed.enabled is true" + + 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" "observationFeed.channel is telegram" + + local feed_to + feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")" + assert_eq "123456789" "$feed_to" "observationFeed.to is 123456789" + + # Verify existing config preserved + 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" "Existing workerPort preserved after feed config write" + + HOME="$ORIGINAL_HOME" + FEED_CHANNEL="" + FEED_TARGET_ID="" + FEED_CONFIGURED=false + rm -rf "$fake_home" +} + +test_write_observation_feed_config_writes_json + +test_write_observation_feed_config_skips_when_not_configured() { + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + + # Create minimal config + mkdir -p "${fake_home}/.openclaw" + local config_file="${fake_home}/.openclaw/openclaw.json" + node -e " + require('fs').writeFileSync('${config_file}', JSON.stringify({ plugins: {} }, null, 2)); + " + + FEED_CONFIGURED="false" + + write_observation_feed_config >/dev/null 2>&1 + + # Config should be unchanged — no observationFeed key + local has_feed + has_feed="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries !== undefined);")" + assert_eq "false" "$has_feed" "Config unchanged when FEED_CONFIGURED is false" + + HOME="$ORIGINAL_HOME" + rm -rf "$fake_home" +} + +test_write_observation_feed_config_skips_when_not_configured + +test_write_observation_feed_config_discord() { + 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: { + entries: { + 'claude-mem': { enabled: true, config: {} } + } + } + }; + require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2)); + " + + FEED_CHANNEL="discord" + FEED_TARGET_ID="1234567890123456789" + FEED_CONFIGURED="true" + + write_observation_feed_config >/dev/null 2>&1 + + 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 "discord" "$feed_channel" "Discord channel type written correctly" + + local feed_to + feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")" + assert_eq "1234567890123456789" "$feed_to" "Discord channel ID written correctly" + + HOME="$ORIGINAL_HOME" + FEED_CHANNEL="" + FEED_TARGET_ID="" + FEED_CONFIGURED=false + rm -rf "$fake_home" +} + +test_write_observation_feed_config_discord + +############################################################################### +# Test: print_completion_summary() — shows observation feed status +############################################################################### + +echo "" +echo "=== print_completion_summary() — observation feed ===" + +test_completion_summary_with_feed() { + AI_PROVIDER="claude" + WORKER_PID="" + FEED_CONFIGURED="true" + FEED_CHANNEL="telegram" + FEED_TARGET_ID="123456789" + + local output + output="$(print_completion_summary 2>&1)" + + assert_contains "$output" "telegram" "Summary shows feed channel when configured" + assert_contains "$output" "123456789" "Summary shows feed target when configured" + assert_contains "$output" "What's next" "Summary includes What's next section" + assert_contains "$output" "/claude-mem-feed" "Summary includes feed check command when configured" + + FEED_CONFIGURED=false + FEED_CHANNEL="" + FEED_TARGET_ID="" +} + +test_completion_summary_with_feed + +test_completion_summary_without_feed() { + AI_PROVIDER="claude" + WORKER_PID="" + FEED_CONFIGURED=false + FEED_CHANNEL="" + FEED_TARGET_ID="" + + local output + output="$(print_completion_summary 2>&1)" + + assert_contains "$output" "not configured" "Summary shows 'not configured' when feed skipped" + assert_contains "$output" "What's next" "Summary includes What's next section without feed" + assert_contains "$output" "/claude-mem-status" "Summary includes status check command" + assert_contains "$output" "localhost:37777" "Summary includes viewer URL" +} + +test_completion_summary_without_feed + +############################################################################### +# Test: Channel type instructions exist in install.sh +############################################################################### + +echo "" +echo "=== Channel instructions ===" + +for channel in telegram discord slack signal whatsapp line; do + if grep -qi "$channel" "$INSTALL_SCRIPT"; then + test_pass "Channel '${channel}' instructions exist in install.sh" + else + test_fail "Channel '${channel}' instructions should exist in install.sh" + fi +done + +# Verify specific instruction content +assert_contains "$(grep -A2 'userinfobot' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "userinfobot" "Telegram instructions include @userinfobot" +assert_contains "$(grep -A2 'Developer Mode' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "Developer Mode" "Discord instructions include Developer Mode" +assert_contains "$(grep -A2 'C01ABC2DEFG' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "C01ABC2DEFG" "Slack instructions include sample channel ID" + ############################################################################### # Summary ###############################################################################