Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
23 changes: 23 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,29 @@ 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:-}
tmp="$RUNTIME_FILE.tmp"
{
printf 'POWERMEM_BASE_URL=%s\n' "$remote_url"
if [ -n "$remote_key" ]; then
printf 'POWERMEM_API_KEY=%s\n' "$remote_key"
fi
} > "$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
75 changes: 71 additions & 4 deletions apps/claude-code-plugin/scripts/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,76 @@ 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 only (.mcp.json reset to empty)
# mcp → .mcp.json only (no runtime.env, hooks unconfigured)
# both → both files
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"
mcp_file="$PLUGIN_ROOT/.mcp.json"
tmp="$mcp_file.tmp"
if [ -n "$remote_api_key" ]; then
printf '{"mcpServers":{"powermem":{"type":"http","url":"%s","headers":{"Authorization":"Bearer %s"}}}}' \
"$mcp_url" "$remote_api_key" > "$tmp"
else
printf '{"mcpServers":{"powermem":{"type":"http","url":"%s"}}}' \
"$mcp_url" > "$tmp"
fi
mv "$tmp" "$mcp_file"
echo "Wrote $mcp_file (url=$mcp_url)"
;;
esac

case "$remote_mode" in
hook)
# Reset .mcp.json to empty so MCP is disabled in hook-only mode.
mcp_file="$PLUGIN_ROOT/.mcp.json"
printf '{"mcpServers":{}}' > "$mcp_file"
echo "Reset $mcp_file (MCP disabled)"
;;
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 +329,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 +339,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 +365,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 All @@ -307,7 +374,7 @@ if embedding_provider not in {"default", "huggingface", "ollama", "lmstudio"} an
sys.exit(2)

server_port = env_first("POWERMEM_SERVER_PORT", "POWERMEM_INIT_PORT") or "8848"
server_host = env_first("POWERMEM_SERVER_HOST") or "127.0.0.1"
server_host = env_first("POWERMEM_SERVER_HOST") or "0.0.0.0"
server_workers = env_first("POWERMEM_SERVER_WORKERS") or "1"
server_log_file = env_first("POWERMEM_SERVER_LOG_FILE") or path_value("powermem-server.log")
logging_level = env_first("LOGGING_LEVEL") or "INFO"
Expand Down Expand Up @@ -716,13 +783,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:-0.0.0.0}" --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:-0.0.0.0}" --port "$port" >> "$LOG_FILE" 2>&1 &
fi
pid=$!
write_managed_pid "$pid"
Expand Down
84 changes: 69 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,59 @@ 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): $PLUGIN_ROOT/.mcp.json holds mcpServers.powermem.url
# Both mode: both files populated
hook_url=""
if [ -f "$RUNTIME_FILE" ]; then
# shellcheck disable=SC1090
. "$RUNTIME_FILE"
hook_url="${POWERMEM_BASE_URL:-}"
fi

mcp_url=""
mcp_file="$PLUGIN_ROOT/.mcp.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 +74,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 +101,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