MAESTRO: add TTY detection, --provider/--api-key flags, and curl | bash support to install.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+121
-15
@@ -3,7 +3,13 @@ set -euo pipefail
|
||||
|
||||
# claude-mem OpenClaw Plugin Installer
|
||||
# Installs the claude-mem persistent memory plugin for OpenClaw gateways.
|
||||
# Usage: bash install.sh [--non-interactive]
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash
|
||||
# # Or with options:
|
||||
# curl -fsSL https://raw.githubusercontent.com/thedotmack/claude-mem/main/openclaw/install.sh | bash -s -- --provider=gemini --api-key=YOUR_KEY
|
||||
# # Direct execution:
|
||||
# bash install.sh [--non-interactive] [--provider=claude|gemini|openrouter] [--api-key=KEY]
|
||||
|
||||
###############################################################################
|
||||
# Constants
|
||||
@@ -11,7 +17,68 @@ set -euo pipefail
|
||||
|
||||
readonly MIN_BUN_VERSION="1.1.14"
|
||||
readonly INSTALLER_VERSION="1.0.0"
|
||||
readonly NON_INTERACTIVE="${1:-}"
|
||||
|
||||
###############################################################################
|
||||
# Argument parsing
|
||||
###############################################################################
|
||||
|
||||
NON_INTERACTIVE=""
|
||||
CLI_PROVIDER=""
|
||||
CLI_API_KEY=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--non-interactive)
|
||||
NON_INTERACTIVE="true"
|
||||
shift
|
||||
;;
|
||||
--provider=*)
|
||||
CLI_PROVIDER="${1#--provider=}"
|
||||
shift
|
||||
;;
|
||||
--provider)
|
||||
CLI_PROVIDER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--api-key=*)
|
||||
CLI_API_KEY="${1#--api-key=}"
|
||||
shift
|
||||
;;
|
||||
--api-key)
|
||||
CLI_API_KEY="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
###############################################################################
|
||||
# TTY detection — ensure interactive prompts work under curl | bash
|
||||
# When piped, stdin reads from curl's output, not the terminal.
|
||||
# We open /dev/tty on fd 3 and read interactive input from there.
|
||||
###############################################################################
|
||||
|
||||
TTY_FD=0
|
||||
|
||||
setup_tty() {
|
||||
if [[ -t 0 ]]; then
|
||||
# stdin IS a terminal — use it directly
|
||||
TTY_FD=0
|
||||
elif [[ -e /dev/tty ]]; then
|
||||
# stdin is piped (curl | bash) but /dev/tty is available
|
||||
exec 3</dev/tty
|
||||
TTY_FD=3
|
||||
else
|
||||
# No terminal available at all
|
||||
if [[ "$NON_INTERACTIVE" != "true" ]]; then
|
||||
echo "Error: No terminal available for interactive prompts." >&2
|
||||
echo "Use --non-interactive or run directly: bash install.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Color utilities — auto-detect terminal color support
|
||||
@@ -43,17 +110,20 @@ warn() { echo -e "${COLOR_YELLOW}⚠${COLOR_RESET} $*"; }
|
||||
error() { echo -e "${COLOR_RED}✗${COLOR_RESET} $*" >&2; }
|
||||
|
||||
prompt_user() {
|
||||
if [[ "$NON_INTERACTIVE" == "--non-interactive" ]]; then
|
||||
if [[ "$NON_INTERACTIVE" == "true" ]]; 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} $* "
|
||||
}
|
||||
|
||||
# Read a line from the terminal (works even when stdin is piped from curl)
|
||||
# Callers always pass -r via $@; shellcheck can't see through the delegation
|
||||
read_tty() {
|
||||
# shellcheck disable=SC2162
|
||||
read "$@" <&"$TTY_FD"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Banner
|
||||
###############################################################################
|
||||
@@ -500,7 +570,42 @@ setup_ai_provider() {
|
||||
info "AI Provider Configuration"
|
||||
echo ""
|
||||
|
||||
if [[ "$NON_INTERACTIVE" == "--non-interactive" ]] || [[ ! -t 0 ]]; then
|
||||
# Handle --provider flag (pre-selected via CLI)
|
||||
if [[ -n "$CLI_PROVIDER" ]]; then
|
||||
case "$CLI_PROVIDER" in
|
||||
claude)
|
||||
AI_PROVIDER="claude"
|
||||
success "Selected via --provider: Claude Max Plan (CLI authentication)"
|
||||
;;
|
||||
gemini)
|
||||
AI_PROVIDER="gemini"
|
||||
AI_PROVIDER_API_KEY="${CLI_API_KEY}"
|
||||
if [[ -n "$AI_PROVIDER_API_KEY" ]]; then
|
||||
success "Selected via --provider: Gemini (API key set via --api-key)"
|
||||
else
|
||||
warn "Selected via --provider: Gemini (no API key — add later in ~/.claude-mem/settings.json)"
|
||||
fi
|
||||
;;
|
||||
openrouter)
|
||||
AI_PROVIDER="openrouter"
|
||||
AI_PROVIDER_API_KEY="${CLI_API_KEY}"
|
||||
if [[ -n "$AI_PROVIDER_API_KEY" ]]; then
|
||||
success "Selected via --provider: OpenRouter (API key set via --api-key)"
|
||||
else
|
||||
warn "Selected via --provider: OpenRouter (no API key — add later in ~/.claude-mem/settings.json)"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
error "Unknown provider: ${CLI_PROVIDER}"
|
||||
error "Valid providers: claude, gemini, openrouter"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Handle non-interactive mode (no --provider flag)
|
||||
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
||||
info "Non-interactive mode: defaulting to Claude Max Plan (no API key needed)"
|
||||
AI_PROVIDER="claude"
|
||||
return 0
|
||||
@@ -521,7 +626,7 @@ setup_ai_provider() {
|
||||
local choice
|
||||
while true; do
|
||||
prompt_user "Enter choice [1/2/3] (default: 1):"
|
||||
read -r choice
|
||||
read_tty -r choice
|
||||
choice="${choice:-1}"
|
||||
|
||||
case "$choice" in
|
||||
@@ -534,7 +639,7 @@ setup_ai_provider() {
|
||||
AI_PROVIDER="gemini"
|
||||
echo ""
|
||||
prompt_user "Enter your Gemini API key (from https://ai.google.dev):"
|
||||
read -rs AI_PROVIDER_API_KEY
|
||||
read_tty -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"
|
||||
@@ -547,7 +652,7 @@ setup_ai_provider() {
|
||||
AI_PROVIDER="openrouter"
|
||||
echo ""
|
||||
prompt_user "Enter your OpenRouter API key (from https://openrouter.ai):"
|
||||
read -rs AI_PROVIDER_API_KEY
|
||||
read_tty -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"
|
||||
@@ -822,7 +927,7 @@ setup_observation_feed() {
|
||||
echo " you'll see it in your chat."
|
||||
echo ""
|
||||
|
||||
if [[ "$NON_INTERACTIVE" == "--non-interactive" ]] || [[ ! -t 0 ]]; then
|
||||
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
||||
info "Non-interactive mode: skipping observation feed setup"
|
||||
info "Configure later in ~/.openclaw/openclaw.json under"
|
||||
info " plugins.entries.claude-mem.config.observationFeed"
|
||||
@@ -831,7 +936,7 @@ setup_observation_feed() {
|
||||
|
||||
prompt_user "Would you like to set up real-time observation streaming to a messaging channel? (y/n)"
|
||||
local answer
|
||||
read -r answer
|
||||
read_tty -r answer
|
||||
answer="${answer:-n}"
|
||||
|
||||
if [[ "${answer,,}" != "y" && "${answer,,}" != "yes" ]]; then
|
||||
@@ -857,7 +962,7 @@ setup_observation_feed() {
|
||||
local channel_choice
|
||||
while true; do
|
||||
prompt_user "Enter choice [1-6]:"
|
||||
read -r channel_choice
|
||||
read_tty -r channel_choice
|
||||
|
||||
case "$channel_choice" in
|
||||
1)
|
||||
@@ -917,7 +1022,7 @@ setup_observation_feed() {
|
||||
|
||||
echo ""
|
||||
prompt_user "Enter your ${FEED_CHANNEL} target ID:"
|
||||
read -r FEED_TARGET_ID
|
||||
read_tty -r FEED_TARGET_ID
|
||||
|
||||
if [[ -z "$FEED_TARGET_ID" ]]; then
|
||||
warn "No target ID provided — skipping observation feed setup."
|
||||
@@ -1098,6 +1203,7 @@ print_completion_summary() {
|
||||
###############################################################################
|
||||
|
||||
main() {
|
||||
setup_tty
|
||||
print_banner
|
||||
detect_platform
|
||||
|
||||
|
||||
@@ -1324,6 +1324,275 @@ assert_contains "$(grep -A2 'userinfobot' "$INSTALL_SCRIPT" 2>/dev/null || echo
|
||||
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"
|
||||
|
||||
###############################################################################
|
||||
# Test: TTY detection — setup_tty() and read_tty() exist
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== TTY detection ==="
|
||||
|
||||
for fn in setup_tty read_tty; 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 TTY_FD is initialized (defaults to 0)
|
||||
if declare -p TTY_FD &>/dev/null; then
|
||||
test_pass "TTY_FD variable is defined"
|
||||
else
|
||||
test_fail "TTY_FD variable should be defined"
|
||||
fi
|
||||
|
||||
# Verify setup_tty is called from main()
|
||||
if grep -q 'setup_tty' "$INSTALL_SCRIPT"; then
|
||||
test_pass "main() calls setup_tty"
|
||||
else
|
||||
test_fail "main() should call setup_tty"
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# Test: Argument parsing — --provider flag
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== Argument parsing — --provider flag ==="
|
||||
|
||||
test_provider_flag_claude() {
|
||||
local result
|
||||
result="$(bash -c '
|
||||
set -euo pipefail
|
||||
TERM=dumb
|
||||
tmp=$(mktemp)
|
||||
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
|
||||
echo "main() { :; }" >> "$tmp"
|
||||
set -- "--provider=claude"
|
||||
source "$tmp"
|
||||
rm -f "$tmp"
|
||||
setup_ai_provider >/dev/null 2>&1
|
||||
echo "$AI_PROVIDER"
|
||||
' 2>/dev/null)" || true
|
||||
|
||||
assert_eq "claude" "$result" "--provider=claude sets AI_PROVIDER to claude"
|
||||
}
|
||||
|
||||
test_provider_flag_claude
|
||||
|
||||
test_provider_flag_gemini_with_api_key() {
|
||||
local result
|
||||
result="$(bash -c '
|
||||
set -euo pipefail
|
||||
TERM=dumb
|
||||
tmp=$(mktemp)
|
||||
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
|
||||
echo "main() { :; }" >> "$tmp"
|
||||
set -- "--provider=gemini" "--api-key=test-key-123"
|
||||
source "$tmp"
|
||||
rm -f "$tmp"
|
||||
setup_ai_provider >/dev/null 2>&1
|
||||
echo "PROVIDER=$AI_PROVIDER"
|
||||
echo "KEY=$AI_PROVIDER_API_KEY"
|
||||
' 2>/dev/null)" || true
|
||||
|
||||
assert_contains "$result" "PROVIDER=gemini" "--provider=gemini sets AI_PROVIDER to gemini"
|
||||
assert_contains "$result" "KEY=test-key-123" "--api-key=test-key-123 sets API key"
|
||||
}
|
||||
|
||||
test_provider_flag_gemini_with_api_key
|
||||
|
||||
test_provider_flag_openrouter() {
|
||||
local result
|
||||
result="$(bash -c '
|
||||
set -euo pipefail
|
||||
TERM=dumb
|
||||
tmp=$(mktemp)
|
||||
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
|
||||
echo "main() { :; }" >> "$tmp"
|
||||
set -- "--provider=openrouter" "--api-key=sk-or-test"
|
||||
source "$tmp"
|
||||
rm -f "$tmp"
|
||||
setup_ai_provider >/dev/null 2>&1
|
||||
echo "PROVIDER=$AI_PROVIDER"
|
||||
echo "KEY=$AI_PROVIDER_API_KEY"
|
||||
' 2>/dev/null)" || true
|
||||
|
||||
assert_contains "$result" "PROVIDER=openrouter" "--provider=openrouter sets AI_PROVIDER"
|
||||
assert_contains "$result" "KEY=sk-or-test" "--api-key sets API key for openrouter"
|
||||
}
|
||||
|
||||
test_provider_flag_openrouter
|
||||
|
||||
test_provider_flag_invalid() {
|
||||
local exit_code=0
|
||||
bash -c '
|
||||
set -euo pipefail
|
||||
TERM=dumb
|
||||
tmp=$(mktemp)
|
||||
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
|
||||
echo "main() { :; }" >> "$tmp"
|
||||
set -- "--provider=invalid"
|
||||
source "$tmp"
|
||||
rm -f "$tmp"
|
||||
setup_ai_provider
|
||||
' >/dev/null 2>&1 || exit_code=$?
|
||||
|
||||
if [[ "$exit_code" -ne 0 ]]; then
|
||||
test_pass "--provider=invalid exits with error"
|
||||
else
|
||||
test_fail "--provider=invalid should exit with error"
|
||||
fi
|
||||
}
|
||||
|
||||
test_provider_flag_invalid
|
||||
|
||||
###############################################################################
|
||||
# Test: Argument parsing — --non-interactive flag (new format)
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== Argument parsing — --non-interactive ==="
|
||||
|
||||
test_non_interactive_flag() {
|
||||
local result
|
||||
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"
|
||||
echo "NON_INTERACTIVE=$NON_INTERACTIVE"
|
||||
' 2>/dev/null)" || true
|
||||
|
||||
assert_contains "$result" "NON_INTERACTIVE=true" "--non-interactive sets NON_INTERACTIVE=true"
|
||||
}
|
||||
|
||||
test_non_interactive_flag
|
||||
|
||||
test_non_interactive_with_provider() {
|
||||
local result
|
||||
result="$(bash -c '
|
||||
set -euo pipefail
|
||||
TERM=dumb
|
||||
tmp=$(mktemp)
|
||||
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
|
||||
echo "main() { :; }" >> "$tmp"
|
||||
set -- "--non-interactive" "--provider=gemini" "--api-key=my-key"
|
||||
source "$tmp"
|
||||
rm -f "$tmp"
|
||||
setup_ai_provider >/dev/null 2>&1
|
||||
echo "PROVIDER=$AI_PROVIDER"
|
||||
echo "KEY=$AI_PROVIDER_API_KEY"
|
||||
echo "NON_INTERACTIVE=$NON_INTERACTIVE"
|
||||
' 2>/dev/null)" || true
|
||||
|
||||
assert_contains "$result" "PROVIDER=gemini" "--non-interactive + --provider: provider set correctly"
|
||||
assert_contains "$result" "KEY=my-key" "--non-interactive + --api-key: key set correctly"
|
||||
assert_contains "$result" "NON_INTERACTIVE=true" "--non-interactive flag parsed alongside --provider"
|
||||
}
|
||||
|
||||
test_non_interactive_with_provider
|
||||
|
||||
###############################################################################
|
||||
# Test: --non-interactive mode completes without hanging
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== --non-interactive full flow ==="
|
||||
|
||||
test_non_interactive_completes() {
|
||||
# Run the full setup_ai_provider + setup_observation_feed in non-interactive mode
|
||||
# This should complete without any prompts or hangs
|
||||
local result
|
||||
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_ai_provider 2>/dev/null
|
||||
setup_observation_feed 2>/dev/null
|
||||
echo "AI=$AI_PROVIDER"
|
||||
echo "FEED=$FEED_CONFIGURED"
|
||||
' 2>/dev/null)" || true
|
||||
|
||||
assert_contains "$result" "AI=claude" "--non-interactive: AI provider defaults to claude"
|
||||
assert_contains "$result" "FEED=false" "--non-interactive: observation feed skipped"
|
||||
}
|
||||
|
||||
test_non_interactive_completes
|
||||
|
||||
###############################################################################
|
||||
# Test: Script structure — curl | bash usage comment
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== curl | bash usage comment ==="
|
||||
|
||||
if grep -q 'curl -fsSL.*raw.githubusercontent.com.*install.sh | bash' "$INSTALL_SCRIPT"; then
|
||||
test_pass "install.sh contains curl | bash usage comment"
|
||||
else
|
||||
test_fail "install.sh should contain curl | bash usage comment"
|
||||
fi
|
||||
|
||||
if grep -q 'bash -s -- --provider=' "$INSTALL_SCRIPT"; then
|
||||
test_pass "install.sh documents --provider flag in usage comment"
|
||||
else
|
||||
test_fail "install.sh should document --provider flag in usage comment"
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# Test: write_settings with --provider flag end-to-end
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== write_settings with --provider flag ==="
|
||||
|
||||
test_write_settings_via_provider_flag() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
|
||||
local result
|
||||
result="$(bash -c '
|
||||
set -euo pipefail
|
||||
TERM=dumb
|
||||
export HOME="'"$fake_home"'"
|
||||
tmp=$(mktemp)
|
||||
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
|
||||
echo "main() { :; }" >> "$tmp"
|
||||
set -- "--provider=gemini" "--api-key=test-end-to-end-key"
|
||||
source "$tmp"
|
||||
rm -f "$tmp"
|
||||
setup_ai_provider >/dev/null 2>&1
|
||||
write_settings >/dev/null 2>&1
|
||||
echo "DONE"
|
||||
' 2>/dev/null)" || true
|
||||
|
||||
if [[ "$result" == *"DONE"* ]]; then
|
||||
local settings_file="${fake_home}/.claude-mem/settings.json"
|
||||
local provider
|
||||
provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
|
||||
assert_eq "gemini" "$provider" "--provider flag: settings.json has provider=gemini"
|
||||
|
||||
local api_key
|
||||
api_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_GEMINI_API_KEY);")"
|
||||
assert_eq "test-end-to-end-key" "$api_key" "--provider flag: settings.json has correct API key"
|
||||
else
|
||||
test_fail "--provider flag: write_settings failed"
|
||||
fi
|
||||
|
||||
rm -rf "$fake_home"
|
||||
}
|
||||
|
||||
test_write_settings_via_provider_flag
|
||||
|
||||
###############################################################################
|
||||
# Summary
|
||||
###############################################################################
|
||||
|
||||
Reference in New Issue
Block a user