MAESTRO: add interactive AI provider setup and settings writer to install.sh
Add setup_ai_provider() with 3-option menu (Claude Max/Gemini/OpenRouter), mask_api_key() for secure terminal display, and write_settings() that generates ~/.claude-mem/settings.json with all 35 defaults from SettingsDefaultsManager.ts in flat JSON schema. Preserves existing user customizations on re-run. 23 new tests added (46/46 total pass). Passes bash -n and shellcheck clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+202
-1
@@ -472,6 +472,199 @@ configure_memory_slot() {
|
||||
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"
|
||||
|
||||
# Build the provider-specific settings
|
||||
local provider_json=""
|
||||
case "$AI_PROVIDER" in
|
||||
claude)
|
||||
provider_json="\"CLAUDE_MEM_PROVIDER\": \"claude\", \"CLAUDE_MEM_CLAUDE_AUTH_METHOD\": \"cli\""
|
||||
;;
|
||||
gemini)
|
||||
provider_json="\"CLAUDE_MEM_PROVIDER\": \"gemini\", \"CLAUDE_MEM_GEMINI_API_KEY\": \"${AI_PROVIDER_API_KEY}\", \"CLAUDE_MEM_GEMINI_MODEL\": \"gemini-2.5-flash-lite\""
|
||||
;;
|
||||
openrouter)
|
||||
provider_json="\"CLAUDE_MEM_PROVIDER\": \"openrouter\", \"CLAUDE_MEM_OPENROUTER_API_KEY\": \"${AI_PROVIDER_API_KEY}\", \"CLAUDE_MEM_OPENROUTER_MODEL\": \"xiaomi/mimo-v2-flash:free\""
|
||||
;;
|
||||
esac
|
||||
|
||||
# Use node to generate settings JSON with all defaults + provider overrides
|
||||
# This ensures the JSON is valid and matches SettingsDefaultsManager.ts defaults
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const homedir = require('os').homedir();
|
||||
|
||||
// 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: '[]'
|
||||
};
|
||||
|
||||
// Apply provider-specific overrides
|
||||
const overrides = {${provider_json}};
|
||||
const settings = Object.assign(defaults, overrides);
|
||||
|
||||
// If settings file already exists, merge (preserve user customizations)
|
||||
const settingsPath = '${settings_file}';
|
||||
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}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Main
|
||||
###############################################################################
|
||||
@@ -511,8 +704,16 @@ main() {
|
||||
info "Configuring memory slot..."
|
||||
configure_memory_slot
|
||||
|
||||
# --- Step 5: AI provider setup ---
|
||||
setup_ai_provider
|
||||
|
||||
# --- Step 6: Write settings ---
|
||||
echo ""
|
||||
success "OpenClaw gateway detection and plugin installation complete"
|
||||
info "Writing settings..."
|
||||
write_settings
|
||||
|
||||
echo ""
|
||||
success "OpenClaw gateway detection, plugin installation, and AI provider setup complete"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -313,6 +313,240 @@ done
|
||||
# Verify the CLAUDE_MEM_REPO constant
|
||||
assert_contains "$CLAUDE_MEM_REPO" "github.com/thedotmack/claude-mem" "CLAUDE_MEM_REPO points to correct repository"
|
||||
|
||||
# Verify AI provider functions exist
|
||||
for fn in setup_ai_provider write_settings mask_api_key; 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: mask_api_key()
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== mask_api_key() ==="
|
||||
|
||||
masked=$(mask_api_key "sk-1234567890abcdef")
|
||||
assert_eq "***************cdef" "$masked" "mask_api_key masks all but last 4 chars"
|
||||
|
||||
masked_short=$(mask_api_key "abcd")
|
||||
assert_eq "****" "$masked_short" "mask_api_key masks keys <= 4 chars entirely"
|
||||
|
||||
masked_five=$(mask_api_key "12345")
|
||||
assert_eq "*2345" "$masked_five" "mask_api_key masks 5-char key correctly"
|
||||
|
||||
###############################################################################
|
||||
# Test: setup_ai_provider() — non-interactive mode defaults to Claude
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== setup_ai_provider() ==="
|
||||
|
||||
test_setup_ai_provider_non_interactive() {
|
||||
# NON_INTERACTIVE is readonly, so test in a child bash that sources with --non-interactive
|
||||
local ai_result
|
||||
ai_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 >/dev/null 2>&1
|
||||
echo "$AI_PROVIDER"
|
||||
' 2>/dev/null)" || true
|
||||
|
||||
assert_eq "claude" "$ai_result" "Non-interactive mode defaults to claude provider"
|
||||
}
|
||||
|
||||
test_setup_ai_provider_non_interactive
|
||||
|
||||
###############################################################################
|
||||
# Test: write_settings() — creates new settings.json with defaults
|
||||
###############################################################################
|
||||
|
||||
echo ""
|
||||
echo "=== write_settings() ==="
|
||||
|
||||
test_write_settings_new_file() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
AI_PROVIDER="claude"
|
||||
AI_PROVIDER_API_KEY=""
|
||||
|
||||
write_settings >/dev/null 2>&1
|
||||
|
||||
local settings_file="${fake_home}/.claude-mem/settings.json"
|
||||
assert_file_exists "$settings_file" "settings.json created at ~/.claude-mem/settings.json"
|
||||
|
||||
# Verify it's valid JSON with expected defaults
|
||||
local provider
|
||||
provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
|
||||
assert_eq "claude" "$provider" "CLAUDE_MEM_PROVIDER set to claude"
|
||||
|
||||
local auth_method
|
||||
auth_method="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_CLAUDE_AUTH_METHOD);")"
|
||||
assert_eq "cli" "$auth_method" "CLAUDE_MEM_CLAUDE_AUTH_METHOD set to cli for Claude provider"
|
||||
|
||||
local worker_port
|
||||
worker_port="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_WORKER_PORT);")"
|
||||
assert_eq "37777" "$worker_port" "CLAUDE_MEM_WORKER_PORT defaults to 37777"
|
||||
|
||||
local model
|
||||
model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_MODEL);")"
|
||||
assert_eq "claude-sonnet-4-5" "$model" "CLAUDE_MEM_MODEL defaults to claude-sonnet-4-5"
|
||||
|
||||
HOME="$ORIGINAL_HOME"
|
||||
rm -rf "$fake_home"
|
||||
}
|
||||
|
||||
test_write_settings_new_file
|
||||
|
||||
# Test: write_settings() — Gemini provider with API key
|
||||
test_write_settings_gemini() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
AI_PROVIDER="gemini"
|
||||
AI_PROVIDER_API_KEY="test-gemini-key-1234"
|
||||
|
||||
write_settings >/dev/null 2>&1
|
||||
|
||||
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" "Gemini: CLAUDE_MEM_PROVIDER set to 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-gemini-key-1234" "$api_key" "Gemini: API key stored in settings"
|
||||
|
||||
local gemini_model
|
||||
gemini_model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_GEMINI_MODEL);")"
|
||||
assert_eq "gemini-2.5-flash-lite" "$gemini_model" "Gemini: model defaults to gemini-2.5-flash-lite"
|
||||
|
||||
HOME="$ORIGINAL_HOME"
|
||||
rm -rf "$fake_home"
|
||||
}
|
||||
|
||||
test_write_settings_gemini
|
||||
|
||||
# Test: write_settings() — OpenRouter provider with API key
|
||||
test_write_settings_openrouter() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
AI_PROVIDER="openrouter"
|
||||
AI_PROVIDER_API_KEY="sk-or-test-key-5678"
|
||||
|
||||
write_settings >/dev/null 2>&1
|
||||
|
||||
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 "openrouter" "$provider" "OpenRouter: CLAUDE_MEM_PROVIDER set to openrouter"
|
||||
|
||||
local api_key
|
||||
api_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_OPENROUTER_API_KEY);")"
|
||||
assert_eq "sk-or-test-key-5678" "$api_key" "OpenRouter: API key stored in settings"
|
||||
|
||||
local or_model
|
||||
or_model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_OPENROUTER_MODEL);")"
|
||||
assert_eq "xiaomi/mimo-v2-flash:free" "$or_model" "OpenRouter: model defaults to xiaomi/mimo-v2-flash:free"
|
||||
|
||||
HOME="$ORIGINAL_HOME"
|
||||
rm -rf "$fake_home"
|
||||
}
|
||||
|
||||
test_write_settings_openrouter
|
||||
|
||||
# Test: write_settings() — preserves existing user customizations
|
||||
test_write_settings_preserves_existing() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
|
||||
# Create existing settings with custom values
|
||||
mkdir -p "${fake_home}/.claude-mem"
|
||||
local settings_file="${fake_home}/.claude-mem/settings.json"
|
||||
node -e "
|
||||
const settings = {
|
||||
CLAUDE_MEM_PROVIDER: 'gemini',
|
||||
CLAUDE_MEM_GEMINI_API_KEY: 'old-key',
|
||||
CLAUDE_MEM_WORKER_PORT: '38888',
|
||||
CLAUDE_MEM_LOG_LEVEL: 'DEBUG'
|
||||
};
|
||||
require('fs').writeFileSync('${settings_file}', JSON.stringify(settings, null, 2));
|
||||
"
|
||||
|
||||
# Now run write_settings with a new provider
|
||||
AI_PROVIDER="claude"
|
||||
AI_PROVIDER_API_KEY=""
|
||||
write_settings >/dev/null 2>&1
|
||||
|
||||
# Provider should be updated to claude
|
||||
local provider
|
||||
provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
|
||||
assert_eq "claude" "$provider" "Preserve: provider updated to new selection"
|
||||
|
||||
# Custom port should be preserved (not overwritten by defaults)
|
||||
local custom_port
|
||||
custom_port="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_WORKER_PORT);")"
|
||||
assert_eq "38888" "$custom_port" "Preserve: existing custom WORKER_PORT preserved"
|
||||
|
||||
# Custom log level should be preserved
|
||||
local log_level
|
||||
log_level="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_LOG_LEVEL);")"
|
||||
assert_eq "DEBUG" "$log_level" "Preserve: existing custom LOG_LEVEL preserved"
|
||||
|
||||
HOME="$ORIGINAL_HOME"
|
||||
rm -rf "$fake_home"
|
||||
}
|
||||
|
||||
test_write_settings_preserves_existing
|
||||
|
||||
# Test: write_settings() — flat schema has all expected keys
|
||||
test_write_settings_complete_schema() {
|
||||
local fake_home
|
||||
fake_home="$(mktemp -d)"
|
||||
HOME="$fake_home"
|
||||
AI_PROVIDER="claude"
|
||||
AI_PROVIDER_API_KEY=""
|
||||
|
||||
write_settings >/dev/null 2>&1
|
||||
|
||||
local settings_file="${fake_home}/.claude-mem/settings.json"
|
||||
|
||||
# Verify key count matches SettingsDefaultsManager (34 keys)
|
||||
local key_count
|
||||
key_count="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(Object.keys(s).length);")"
|
||||
|
||||
# Settings should have all 34 keys from SettingsDefaultsManager
|
||||
if (( key_count >= 30 )); then
|
||||
test_pass "Settings file has ${key_count} keys (complete schema)"
|
||||
else
|
||||
test_fail "Settings file has ${key_count} keys, expected >= 30" "Schema may be incomplete"
|
||||
fi
|
||||
|
||||
# Verify it does NOT have nested { env: {...} } format
|
||||
local has_env_key
|
||||
has_env_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.env !== undefined);")"
|
||||
assert_eq "false" "$has_env_key" "Settings uses flat schema (no nested 'env' key)"
|
||||
|
||||
HOME="$ORIGINAL_HOME"
|
||||
rm -rf "$fake_home"
|
||||
}
|
||||
|
||||
test_write_settings_complete_schema
|
||||
|
||||
###############################################################################
|
||||
# Summary
|
||||
###############################################################################
|
||||
|
||||
Reference in New Issue
Block a user