Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions apps/claude-code-plugin/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ sh "$CLAUDE_PLUGIN_ROOT/scripts/..."
first, then retry `/memory-powermem:init`.
2. Run `sh "$CLAUDE_PLUGIN_ROOT/scripts/status.sh"` and inspect whether config,
uv, managed PID, Python versions, and health are present.
3. If `.env` is missing, run init with auto-detection first:
3. **If the server is healthy and `.env` exists** — tell the user the current
storage backend (read `DATABASE_PROVIDER` from `.env`). Do not re-run `init.sh`.
If the user wants to reconfigure, stop the server first, then proceed.
4. If `.env` is missing, run init with auto-detection first:

```bash
sh "$CLAUDE_PLUGIN_ROOT/scripts/init.sh"
Expand All @@ -83,7 +86,7 @@ sh "$CLAUDE_PLUGIN_ROOT/scripts/..."
OceanBase/seekdb production use), local HuggingFace embedding (no API key,
`sentence-transformers` from `powermem[extras]`), server settings, and logging
settings.
4. If init reports missing values, ask the user only for those missing values. Do
5. If init reports missing values, ask the user only for those missing values. Do
not invent credentials. Re-run init with the matching environment variables:

```bash
Expand Down Expand Up @@ -114,11 +117,11 @@ sh "$CLAUDE_PLUGIN_ROOT/scripts/..."
`powermem[server,extras] @ git+https://github.com/oceanbase/powermem.git@<branch-or-sha>`.
- `POWERMEM_INIT_PYTHON` to force a specific Python >= 3.11.
- `POWERMEM_INIT_PORT` to force the managed server port.
5. Never print API keys, auth tokens, or other credentials. Mask any secret in
6. Never print API keys, auth tokens, or other credentials. Mask any secret in
summaries.
6. After init succeeds, run `sh "$CLAUDE_PLUGIN_ROOT/scripts/status.sh"` again and
7. After init succeeds, run `sh "$CLAUDE_PLUGIN_ROOT/scripts/status.sh"` again and
report the base URL.
7. The hook launcher reads `runtime.env`, so once init writes a base URL, prompt
8. The hook launcher reads `runtime.env`, so once init writes a base URL, prompt
recall and session-save hooks use that backend automatically.

The default local embedding model (`all-MiniLM-L6-v2`) is downloaded
Expand Down
6 changes: 6 additions & 0 deletions apps/claude-code-plugin/hooks/run-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ load_env_file() {
}
load_env_file "$DATA_DIR/runtime.env"
load_env_file "$PLUGIN_ROOT/config/runtime.env"
# MCP-only mode: runtime.env sets POWERMEM_HOOK_DISABLED=1 so the native
# binary never runs (and never falls back to a stale POWERMEM_BASE_URL).
# Must be checked AFTER loading runtime.env so the marker takes effect.
if [ "${POWERMEM_HOOK_DISABLED:-0}" = "1" ]; then
exit 0
fi
case "$(uname -s 2>/dev/null)" in
Darwin) GOOS=darwin ;;
Linux) GOOS=linux ;;
Expand Down
77 changes: 77 additions & 0 deletions apps/claude-code-plugin/scripts/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,43 @@ write_runtime_base_url() {
mv "$tmp" "$RUNTIME_FILE"
}

# Write runtime.env for remote-server mode (no local .env, no local PID).
# Args: base_url [api_key]
write_runtime_remote() {
remote_url=$1
remote_key=${2:-}
# Single-quote values so URLs / keys with shell metacharacters ($, ;, spaces,
# backticks, etc.) survive being sourced by run-hook.sh and status.sh.
# Embedded single quotes are escaped via the standard '\'' trick.
sq_url=$(printf '%s' "$remote_url" | sed "s/'/'\\\\''/g")
tmp="$RUNTIME_FILE.tmp"
{
printf "POWERMEM_BASE_URL='%s'\n" "$sq_url"
if [ -n "$remote_key" ]; then
sq_key=$(printf '%s' "$remote_key" | sed "s/'/'\\\\''/g")
printf "POWERMEM_API_KEY='%s'\n" "$sq_key"
fi
} > "$tmp"
mv "$tmp" "$RUNTIME_FILE"
}

# Write runtime.env for MCP-only mode: no base URL, hooks disabled.
# Overwrites any stale POWERMEM_BASE_URL so run-hook.sh exits early instead
# of hitting the previous remote/local URL that no longer applies.
write_runtime_hook_disabled() {
tmp="$RUNTIME_FILE.tmp"
printf 'POWERMEM_HOOK_DISABLED=1\n' > "$tmp"
mv "$tmp" "$RUNTIME_FILE"
}

# Return 0 if the given URL points at a remote host (not localhost/127.0.0.1).
is_remote_url() {
case "$1" in
http://localhost:*|http://127.0.0.1:*|https://localhost:*|https://127.0.0.1:*) return 1 ;;
*) return 0 ;;
esac
}

export_env_file_vars() {
env_file=$1
[ -f "$env_file" ] || return 0
Expand Down Expand Up @@ -513,3 +550,43 @@ find_free_port() {
done
return 1
}

# --- User-level MCP config ---
#
# We write the powermem MCP entry to the user-scope config so it persists
# across plugin reinstalls (the plugin cache .mcp.json is volatile — wiped
# on every uninstall+install) and applies to all projects.
#
# Implemented via the `claude mcp` CLI so the storage location tracks
# whatever Claude Code uses for the current version / platform / config
# dir, rather than hardcoding ~/.claude.json.
#
# Usage:
# write_user_mcp_config <url> [api_key]
# remove_user_mcp_config

write_user_mcp_config() {
mcp_url="$1"
mcp_api_key="${2:-}"
if ! command -v claude >/dev/null 2>&1; then
echo "ERROR: 'claude' CLI not found on PATH; cannot configure user-scope MCP." >&2
echo "Run 'claude mcp add --scope user --transport http powermem \"$mcp_url\"' manually." >&2
return 1
fi
# Remove any existing entry first so the add is idempotent and stale
# headers don't linger when the API key changes.
claude mcp remove powermem --scope user >/dev/null 2>&1 || true
if [ -n "$mcp_api_key" ]; then
claude mcp add --scope user --transport http powermem "$mcp_url" \
--header "Authorization: Bearer $mcp_api_key" >/dev/null
else
claude mcp add --scope user --transport http powermem "$mcp_url" >/dev/null
fi
}

remove_user_mcp_config() {
if ! command -v claude >/dev/null 2>&1; then
return 0
fi
claude mcp remove powermem --scope user >/dev/null 2>&1 || true
}
74 changes: 71 additions & 3 deletions apps/claude-code-plugin/scripts/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,77 @@ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
echo "PowerMem Claude Code plugin init"
echo "Data dir: $DATA_DIR"

# --- Remote mode short-circuit ---
# Triggered when the user provides a remote PowerMem server URL via
# POWERMEM_INIT_BASE_URL (AskUserQuestion) or POWERMEM_BASE_URL (env, non-localhost).
# Skips .env creation, uvx launch, PID management.
# Connection mode (POWERMEM_INIT_CONNECTION_MODE=hook|mcp|both, default both)
# decides which files get written:
# hook → runtime.env with URL + key; remove powermem from user-level mcpServers
# mcp → user-level mcpServers only; runtime.env gets POWERMEM_HOOK_DISABLED=1
# so the hook launcher exits early instead of falling back to a stale URL
# both → both sources populated, hook enabled
# MCP config is written to the user-scope config (~/.claude.json top-level
# mcpServers) so it survives plugin reinstalls and applies to all projects.
remote_init_url="${POWERMEM_INIT_BASE_URL:-${POWERMEM_BASE_URL:-}}"
if [ -n "$remote_init_url" ] && is_remote_url "$remote_init_url"; then
remote_api_key="${POWERMEM_INIT_API_KEY:-${POWERMEM_API_KEY:-}}"
remote_mode="${POWERMEM_INIT_CONNECTION_MODE:-both}"
case "$remote_mode" in
hook|mcp|both) ;;
*) echo "ERROR: POWERMEM_INIT_CONNECTION_MODE must be hook, mcp, or both (got: $remote_mode)" >&2; exit 1 ;;
esac
echo "Remote server mode: $remote_init_url (connection: $remote_mode)"
mkdir -p "$DATA_DIR"

echo "Verifying connectivity..."
if ! is_healthy "$remote_init_url"; then
echo "ERROR: remote server at $remote_init_url is not healthy." >&2
echo "Check the URL and any required API key." >&2
exit 1
fi
echo "Remote server healthy: $remote_init_url"

case "$remote_mode" in
hook|both)
write_runtime_remote "$remote_init_url" "$remote_api_key"
echo "Wrote $RUNTIME_FILE"
;;
esac

case "$remote_mode" in
mcp|both)
mcp_url=$(printf '%s' "$remote_init_url" | sed 's:/*$::')"/mcp"
write_user_mcp_config "$mcp_url" "$remote_api_key"
echo "Wrote powermem MCP server to user-scope config (url=$mcp_url)"
;;
esac

case "$remote_mode" in
hook)
# Remove powermem from user-level mcpServers so MCP is disabled in
# hook-only mode. Other MCP servers are preserved.
remove_user_mcp_config
echo "Removed powermem from user-scope config (MCP disabled)"
;;
mcp)
# Write a marker runtime.env so run-hook.sh exits early instead of
# falling back to a stale POWERMEM_BASE_URL. Without this, the hook
# binary would run with whatever URL was last configured.
write_runtime_hook_disabled
echo "Wrote $RUNTIME_FILE (hook disabled; MCP-only mode)"
;;
esac

exit 0
fi

base_url=$(runtime_base_url)

ensure_bootstrap_python || exit 1
echo "Bootstrap Python: $BOOTSTRAP_PYTHON ($(python_version "$BOOTSTRAP_PYTHON"))"

# Interactive configuration prompts.
create_env_file() {
"$BOOTSTRAP_PYTHON" - "$ENV_FILE" "$DATA_DIR" <<'PY'
import json
Expand Down Expand Up @@ -264,6 +330,7 @@ embedding_provider = env_first("POWERMEM_INIT_EMBEDDING_PROVIDER", "EMBEDDING_PR
embedding_provider = embedding_provider.lower()

embedding_model_defaults = {
"none": "none",
"default": "all-MiniLM-L6-v2",
"huggingface": "all-MiniLM-L6-v2",
"qwen": "text-embedding-v4",
Expand All @@ -273,6 +340,7 @@ embedding_model_defaults = {
"lmstudio": "text-embedding-nomic-embed-text-v1.5",
}
embedding_dim_defaults = {
"none": "0",
"default": "384",
"huggingface": "384",
"qwen": "1536",
Expand All @@ -298,7 +366,7 @@ if not embedding_api_key:
elif embedding_provider == "siliconflow":
embedding_api_key = env_first("SILICONFLOW_API_KEY") or settings_first(settings_env, "SILICONFLOW_API_KEY")

if embedding_provider not in {"default", "huggingface", "ollama", "lmstudio"} and not embedding_api_key:
if embedding_provider not in {"none", "default", "huggingface", "ollama", "lmstudio"} and not embedding_api_key:
print(
"Missing configuration: POWERMEM_INIT_EMBEDDING_API_KEY "
f"for EMBEDDING_PROVIDER={embedding_provider}",
Expand Down Expand Up @@ -716,13 +784,13 @@ if [ -n "${POWERMEM_UV_INDEX_URL:-}" ]; then
--default-index "$POWERMEM_UV_INDEX_URL" \
--from "$PACKAGE" \
$UVX_WITH_ARGS \
powermem-server --host 127.0.0.1 --port "$port" >> "$LOG_FILE" 2>&1 &
powermem-server --host "${POWERMEM_SERVER_HOST:-127.0.0.1}" --port "$port" >> "$LOG_FILE" 2>&1 &
else
POWERMEM_ENV_FILE="$ENV_FILE" nohup "$UV_BIN" tool run \
--python "$BOOTSTRAP_PYTHON" \
--from "$PACKAGE" \
$UVX_WITH_ARGS \
powermem-server --host 127.0.0.1 --port "$port" >> "$LOG_FILE" 2>&1 &
powermem-server --host "${POWERMEM_SERVER_HOST:-127.0.0.1}" --port "$port" >> "$LOG_FILE" 2>&1 &
fi
pid=$!
write_managed_pid "$pid"
Expand Down
86 changes: 71 additions & 15 deletions apps/claude-code-plugin/scripts/status.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,61 @@ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
# shellcheck disable=SC1091
. "$SCRIPT_DIR/common.sh"

base_url=$(runtime_base_url)
# --- Discover connection configuration ---
# Hook mode (REST): runtime.env holds POWERMEM_BASE_URL (+ optional POWERMEM_API_KEY)
# MCP mode (http): user-scope ~/.claude.json holds mcpServers.powermem.url
# (written via `claude mcp add --scope user`, survives plugin
# reinstalls; the plugin cache .mcp.json is volatile and unused)
# Both mode: both sources populated
hook_url=""
if [ -f "$RUNTIME_FILE" ]; then
# shellcheck disable=SC1090
. "$RUNTIME_FILE"
hook_url="${POWERMEM_BASE_URL:-}"
fi

mcp_url=""
mcp_file="${HOME:-}/.claude.json"
if [ -f "$mcp_file" ]; then
if BOOTSTRAP_PYTHON=$(choose_python 2>/dev/null); then
mcp_url=$("$BOOTSTRAP_PYTHON" - "$mcp_file" <<'PY' 2>/dev/null || true
import json, sys
try:
data = json.load(open(sys.argv[1]))
srv = data.get("mcpServers", {}).get("powermem", {})
print(srv.get("url", ""))
except Exception:
pass
PY
)
fi
fi

if [ -n "$hook_url" ] && [ -n "$mcp_url" ]; then
mode="both"
elif [ -n "$hook_url" ]; then
mode="hook"
elif [ -n "$mcp_url" ]; then
mode="mcp"
else
mode="none"
fi

# Derive a base URL for health checks. Prefer hook_url; fall back to MCP url
# with the trailing /mcp stripped.
health_url="$hook_url"
if [ -z "$health_url" ] && [ -n "$mcp_url" ]; then
health_url=$(printf '%s' "$mcp_url" | sed -E 's#/mcp$##')
fi

echo "PowerMem Claude Code plugin status"
echo "Data dir: $DATA_DIR"
echo "Runtime file: $RUNTIME_FILE"
echo "Env file: $ENV_FILE"
echo "PID file: $(managed_pid_file)"
echo "Base URL: $base_url"
echo "Connection mode: $mode"
[ -n "$hook_url" ] && echo "Hook base URL: $hook_url"
[ -n "$mcp_url" ] && echo "MCP URL: $mcp_url"

if BOOTSTRAP_PYTHON=$(choose_python 2>/dev/null); then
echo "Bootstrap Python: $BOOTSTRAP_PYTHON ($(python_version "$BOOTSTRAP_PYTHON"))"
Expand All @@ -29,7 +76,12 @@ fi
if pid_alive; then
echo "Managed server PID: $(managed_pid)"
else
echo "Managed server PID: not running"
case "$mode" in
mcp) echo "Managed server PID: not running (MCP-only mode, no local server expected)" ;;
hook) echo "Managed server PID: not running (hook mode against remote, no local server expected)" ;;
both) echo "Managed server PID: not running (remote mode, no local server expected)" ;;
*) echo "Managed server PID: not running" ;;
esac
fi

if [ -n "${POWERMEM_UV_BIN:-}" ] && command -v "$POWERMEM_UV_BIN" >/dev/null 2>&1; then
Expand All @@ -51,19 +103,23 @@ if [ -d "$DATA_DIR/venv" ]; then
echo "Legacy venv: $DATA_DIR/venv (unused by uvx init)"
fi

if is_healthy "$base_url"; then
echo "Health: healthy"
if [ -z "$health_url" ]; then
echo "Health: no base URL configured (run /memory-powermem:init)"
else
echo "Health: unavailable"
case "$base_url" in
http://localhost:*|http://127.0.0.1:*)
port=$(printf '%s\n' "$base_url" | sed -E 's#^http://(localhost|127\.0\.0\.1):([0-9]+).*#\2#')
case "$port" in
*[!0-9]*|"") ;;
*) describe_port "$port" ;;
esac
;;
esac
if is_healthy "$health_url"; then
echo "Health: healthy ($health_url)"
else
echo "Health: unavailable ($health_url)"
case "$health_url" in
http://localhost:*|http://127.0.0.1:*)
port=$(printf '%s\n' "$health_url" | sed -E 's#^http://(localhost|127\.0\.0\.1):([0-9]+).*#\2#')
case "$port" in
*[!0-9]*|"") ;;
*) describe_port "$port" ;;
esac
;;
esac
fi
fi

if [ -f "$LOG_FILE" ]; then
Expand Down
Loading
Loading