Skip to content

feat(init): AskUserQuestion interactive flow + remote-init mode + dashboard 0.0.0.0 - #1101

Merged
wayyoungboy merged 9 commits into
oceanbase:mainfrom
lightzt99:feat/init-askuser-interactive
Jun 29, 2026
Merged

feat(init): AskUserQuestion interactive flow + remote-init mode + dashboard 0.0.0.0#1101
wayyoungboy merged 9 commits into
oceanbase:mainfrom
lightzt99:feat/init-askuser-interactive

Conversation

@lightzt99

@lightzt99 lightzt99 commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replace shell-level interactive prompts in init.sh with a 3-round AskUserQuestion flow (Server → Storage+LLM → Embedding) driven from the init SKILL, so decisions are made in the Claude Code UI instead of the terminal.
  • Split the Server question (Round 1) into three options: Local — loopback (127.0.0.1), Local — all interfaces (0.0.0.0), and Remote. Loopback remains the script default; all-interface binding is an explicit user choice, mapped to POWERMEM_SERVER_HOST.
  • Add a remote-init short-circuit: when the user supplies a remote PowerMem server URL, init.sh skips .env / uvx / PID management and writes only runtime.env and/or .mcp.json per POWERMEM_INIT_CONNECTION_MODE (hook | mcp | both).
  • Make status.sh / status SKILL mode-aware (hook | mcp | both | none) by reading both runtime.env (hook URL) and ${PLUGIN_ROOT}/.mcp.json (MCP URL).
  • Extract shared model-cache / CN-mirror download helpers (is_model_cached, detect_country, download_via_modelscope, bridge_modelscope_to_hf_cache) from pyseekdb_default.py into a new _model_cache.py module so both pyseekdb_default and huggingface share the same logic.

Changes

Plugin scripts (apps/claude-code-plugin/scripts/)

  • common.sh — shared helpers: is_remote_url, is_healthy, write_runtime_remote.
  • init.sh — remote-init short-circuit; dashboard host ${POWERMEM_SERVER_HOST:-127.0.0.1} (loopback default, 0.0.0.0 opt-in via env or Question 0); connection-mode switch.
  • status.sh — mode-aware reporting from runtime.env + .mcp.json.

SKILL files (apps/claude-code-plugin/skills/)

  • init/SKILL.md — 3-round AskUserQuestion flow; Round 1 splits Local into loopback / all-interfaces options; skip Storage question when server healthy; CLAUDE_PLUGIN_ROOT resolved per-invocation.
  • status/SKILL.md — documents the new hook|mcp|both|none mode reporting.

Embedding refactor (src/powermem/integrations/embeddings/)

  • _model_cache.py (new) — shared cache/mirror helpers extracted from pyseekdb_default.py.
  • pyseekdb_default.py — imports from _model_cache.
  • huggingface.py — adopts the shared cache/mirror helpers.
  • tests/unit/test_pyseekdb_default_download.py — patches _model_cache instead of pyseekdb_default internals.

Tests

  • tests/unit/test_claude_plugin_uv_install.py — updated uvx-launcher assertion to the parameterized form --host "${POWERMEM_SERVER_HOST:-127.0.0.1}" to match the new default + override contract.

Test plan

  • pytest tests/unit/test_claude_plugin_uv_install.py — uvx-launcher assertion matches the new parameterized host form.
  • pytest tests/unit/test_pyseekdb_default_download.py — download / CN-mirror tests pass against the extracted module.
  • CI test (3.11), test (3.12), claude-hook-regression all green on 049627a.
  • Manual: install plugin from this branch, run /init, confirm Server → Storage+LLM → Embedding questions appear in the Claude Code UI (not the terminal).
  • Manual: with POWERMEM_INIT_BASE_URL=https://remote:8848, confirm init.sh writes runtime.env and/or .mcp.json without starting a local server.
  • Manual: confirm dashboard binds 127.0.0.1:8848 by default; choosing "Local — all interfaces" in Round 1 (or setting POWERMEM_SERVER_HOST=0.0.0.0) binds 0.0.0.0 and is reachable from another host.
  • Manual: /status reports mode=hook, mode=mcp, mode=both, or mode=none correctly.

- Add ask_user() shell function with 3-layer bypass (NON_INTERACTIVE, pre-set
  var, non-TTY) for users running init.sh from a terminal
- Add prompt_user_config() that asks storage backend (sqlite/oceanbase),
  LLM provider (noop/anthropic/openai/...), and embedding provider (local/cloud)
- Wire into init.sh main flow, right before create_env_file, so answers are
  consumed as POWERMEM_INIT_* env vars by the embedded Python config generator
- Enhance /init skill SKILL.md with explicit AskUserQuestion instructions:
  conditional 3-question flow, env-var mapping, and POWERMEM_NON_INTERACTIVE=1
  invocation template for the Claude Code plugin path
When the server is already running and .env has DATABASE_PROVIDER set,
show the current backend instead of asking the user to reconfigure.
Only ask the Storage question when config is genuinely missing.
init.sh gains a remote-init short-circuit: when POWERMEM_INIT_BASE_URL
(or non-localhost POWERMEM_BASE_URL) is provided, it skips .env, uvx,
and PID management, writing only runtime.env and/or .mcp.json per
POWERMEM_INIT_CONNECTION_MODE (hook|mcp|both, default both).

Dashboard default host changes from 127.0.0.1 to 0.0.0.0, overridable
via POWERMEM_SERVER_HOST, so the dashboard is reachable across
containers / remote hosts by default.

status.sh becomes mode-aware: reads runtime.env (hook_url) and
${PLUGIN_ROOT}/.mcp.json (mcp_url) and reports mode=hook|mcp|both|none.
common.sh gains shared helpers (is_remote_url, is_healthy,
write_runtime_remote) used by both scripts.
init SKILL.md describes a 3-round AskUserQuestion flow (Storage →
LLM → Embedding) that replaces the shell-level interactive prompts.
Each question is asked only when the relevant config is missing,
and CLAUDE_PLUGIN_ROOT is resolved per-invocation rather than
baked into shell env. When the server is already healthy with a
configured DATABASE_PROVIDER, the Storage question is skipped.

status SKILL.md documents the new hook|mcp|both|none mode reporting
and how to read runtime.env / .mcp.json.
Move is_model_cached, detect_country, download_via_modelscope, and
bridge_modelscope_to_hf_cache out of pyseekdb_default.py into a new
_model_cache.py module so both pyseekdb_default and huggingface can
share the same CN-mirror download / cache-bridge logic without
duplication. Tests are updated to patch _model_cache instead of
pyseekdb_default internals.

@wayyoungboy wayyoungboy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting the interactive / remote init flow together. I cannot mark this ready yet for two reasons:

  • CI is red: both test (3.11) and test (3.12) fail in tests/unit/test_claude_plugin_uv_install.py::test_init_uses_uvx_launcher_instead_of_plugin_venv_install. The test still asserts local uvx launch contains powermem-server --host 127.0.0.1 --port "$port", while this PR now launches with ${POWERMEM_SERVER_HOST:-0.0.0.0}. Please either update the test if this behavior is intentional, or keep loopback as the local default and make all-interface binding an explicit opt-in.
  • The new init skill text still describes Local mode as starting a server on 127.0.0.1:8848, but the script and generated config now default local launch to 0.0.0.0. Please make the docs, generated config, and launch behavior agree, especially because binding all interfaces changes the local exposure model.

Once the host contract is settled and the Python test jobs are green, I can re-review the updated head.

Restore 127.0.0.1 as the script default for POWERMEM_SERVER_HOST so
loopback remains the local exposure model. All-interface binding
(0.0.0.0) becomes an explicit user choice: Question 0 of the
AskUserQuestion flow now splits "Local" into two options —
loopback-only and all-interfaces — and maps the answer to
POWERMEM_SERVER_HOST before invoking init.sh.

Update the uvx-launcher test to assert the new parameterized form
`--host "${POWERMEM_SERVER_HOST:-127.0.0.1}"` instead of the old
hardcoded `--host 127.0.0.1`. Addresses PR oceanbase#1101 review feedback.
@lightzt99

Copy link
Copy Markdown
Collaborator Author

Thanks for the review @wayyoungboy. Both points are addressed in 049627a:

1. CI red — test assertion mismatch

Loopback is restored as the local default. init.sh now uses ${POWERMEM_SERVER_HOST:-127.0.0.1} for both .env generation (line 377) and the powermem-server launch lines (786, 792). The uvx-launcher test assertion in tests/unit/test_claude_plugin_uv_install.py:552 is updated to the parameterized form:

assert "powermem-server --host \"${POWERMEM_SERVER_HOST:-127.0.0.1}\" --port \"$port\"" in script

test (3.11), test (3.12), and claude-hook-regression are all green on 049627a.

2. Docs / generated config / launch behavior consistency

All three now agree on 127.0.0.1 as the local default. All-interface binding is an explicit opt-in via Round 1 of the AskUserQuestion flow: the "Local" option is split into two choices — "Local — loopback" (127.0.0.1) and "Local — all interfaces" (0.0.0.0) — and the answer is mapped to POWERMEM_SERVER_HOST before invoking init.sh. The SKILL text, generated .env, and launch command all reflect the chosen host. The exposure model is now an explicit user decision rather than a silent default change.

PR description updated to match. Ready for re-review when you have a moment.

Remote MCP mode was writing to $PLUGIN_ROOT/.mcp.json (the plugin cache
dir), which is volatile — wiped on every plugin uninstall+install — so
the user's remote MCP config was lost on reinstalls. It also could not
take effect without a plugin reload.

Register the powermem MCP server in the user-scope config instead, via
the `claude mcp add --scope user --transport http` CLI. The CLI decides
where to store it (typically ~/.claude.json top-level mcpServers) so the
location tracks the current Claude Code version / platform rather than
being hardcoded. Hook-only mode runs `claude mcp remove powermem
--scope user` to disable MCP, preserving any other MCP servers.

Adds write_user_mcp_config / remove_user_mcp_config helpers in common.sh
that wrap the CLI with a PATH check and idempotent remove-then-add.

@wayyoungboy wayyoungboy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates. The CI jobs I checked are green now, and the loopback/all-interface default issue from the earlier review looks addressed. I still cannot approve the latest head because the new remote connection modes have a few correctness issues:

  • runtime.env writes remote URL/API key values as raw shell assignments, but the hook/status scripts later source that file. URLs or keys containing shell metacharacters or spaces can break the config, and command substitution is evaluated while sourcing.
  • init.sh now registers MCP through claude mcp add --scope user, but status.sh still reads ${PLUGIN_ROOT}/.mcp.json, so remote mcp mode reports none and both reports only hook.
  • Remote mcp mode says hooks are disabled, but it neither removes/stamps runtime.env nor changes the hook launcher/native hook behavior. Existing hook config can remain active, and with no runtime file the native hook still falls back to http://localhost:8848.
  • git diff --check upstream/main...HEAD currently fails on trailing whitespace at EOF in apps/claude-code-plugin/scripts/common.sh.

Local checks run: shell syntax check passed; pytest tests/unit/test_claude_plugin_uv_install.py tests/unit/test_pyseekdb_default_download.py passed with 43 tests.

…ble hook in MCP mode

Four review issues on the user-scope MCP config commit (9f5ac95):

1. write_runtime_remote wrote URLs/keys bare, so shell metacharacters
   ($, ;, spaces, backticks, quotes) broke sourcing by run-hook.sh /
   status.sh. Switch to single-quoted values with '\'' escaping.

2. status.sh read $PLUGIN_ROOT/.mcp.json for the MCP URL, but that file is
   the volatile plugin-cache config (always empty since 9f5ac95 moved
   writes to user-scope). Read ~/.claude.json top-level mcpServers.powermem.url
   instead — the file the `claude mcp` CLI actually writes to.

3. MCP-only mode wrote nothing to runtime.env, leaving stale
   POWERMEM_BASE_URL from a prior hook/both init. The hook binary then
   ran against the old URL. Now write_runtime_hook_disabled() emits
   POWERMEM_HOOK_DISABLED=1, and run-hook.sh checks that marker after
   loading runtime.env and exits 0 before exec'ing the native binary.

4. common.sh had trailing blank lines at EOF (git diff --check).

Also tightens Round 1 remote follow-up AskUserQuestion wording in
SKILL.md: dedup the two URL placeholder options into distinct HTTP/HTTPS
protocol hints; clarify Hook mode works when auth is disabled or a key
was provided. Both connection mode retained per user request.

Adds regression tests for the quoting, hook-disabled marker, run-hook
early exit, and init.sh mcp-branch wiring.
@lightzt99

Copy link
Copy Markdown
Collaborator Author

@wayyoungboy Thanks for the review. All four issues are fixed in 54a25f8:

  1. runtime.env quotingwrite_runtime_remote now single-quotes both POWERMEM_BASE_URL and POWERMEM_API_KEY values, with embedded ' escaped via the standard '\'' trick. URLs/keys with $, ;, spaces, backticks, and quotes survive being sourced by run-hook.sh and status.sh verbatim. Regression test: test_write_runtime_remote_quotes_metacharacters.

  2. status.sh reads the wrong MCP file — switched from $PLUGIN_ROOT/.mcp.json (volatile plugin-cache, always empty since the user-scope move) to ~/.claude.json top-level mcpServers.powermem.url, which is where claude mcp add --scope user actually writes. mcp mode now reports mcp, both reports both.

  3. MCP-only mode didn't disable the hookinit.sh's mcp branch now calls write_runtime_hook_disabled(), which overwrites runtime.env with POWERMEM_HOOK_DISABLED=1 (replacing any stale POWERMEM_BASE_URL). run-hook.sh checks that marker after load_env_file and exits 0 before exec'ing the native binary, so the hook can no longer fall back to http://localhost:8848. Regression tests: test_write_runtime_hook_disabled_writes_marker, test_run_hook_exits_early_when_hook_disabled, test_init_mcp_branch_writes_hook_disabled_marker.

  4. git diff --check trailing whitespace — stripped the trailing blank lines at EOF in common.sh; git diff --check upstream/main...HEAD is now clean.

Local: pytest tests/unit/test_claude_plugin_uv_install.py — 23 passed (was 19). sh -n clean on all four touched scripts. The SKILL.md Round 1 wording was also tightened (dedup'd URL placeholder options, clarified Hook mode applies when auth is disabled or a key was supplied; Both retained). Ready for re-review.

@lightzt99

lightzt99 commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

AskUserQuestion Round 1

F8E8AEC4-F200-4E69-95C0-FA6BE630E41C

AskUserQuestion Round 2 (Remote)

  1. provide server URL
0990A723-4649-42F2-AF1E-9321BA708F08
  1. provide API key if required
0074E5EE-F40B-4791-AD6C-D4561E37EA38
  1. choose connection mode (hook/mcp/both)
F3C60CAA-6643-4BD5-ADCD-F70DBB1C49CF

AskUserQuestion Round 2 (Local)

  1. choose storage backend (oceanbase/sqlite)
5E0C42D4-CF21-416D-AEAE-6EBF2ADB92FD
  1. enable LLM or not
C3F17E9E-942C-4FCE-A83E-CAA7B7DDB20B

AskUserQuestion Round 3 (storage backend = sqlite)

568CA954-4D08-4842-9C21-7E271AA08401

@lightzt99
lightzt99 marked this pull request as ready for review June 29, 2026 09:46
OceanBase requires a vector embedding field to store memories, so
embeddings are mandatory. Offering "None" in the AskUserQuestion Round 3
options for the OceanBase storage path would let users pick an invalid
combination. SQLite keeps "None" since it can fall back to non-vector
storage; OceanBase only offers "Built-in seekdb" and "Cloud (<provider>)".

Also clarifies the mapping line: POWERMEM_INIT_EMBEDDING_PROVIDER=none is
now annotated "SQLite only".
@wayyoungboy
wayyoungboy merged commit 4cee2e1 into oceanbase:main Jun 29, 2026
24 checks passed
222twotwotwo pushed a commit to 222twotwotwo/powercontext that referenced this pull request Aug 23, 2026
…hboard 0.0.0.0 (oceanbase#1101)

* feat(init): add interactive config prompts with AskUserQuestion support

- Add ask_user() shell function with 3-layer bypass (NON_INTERACTIVE, pre-set
  var, non-TTY) for users running init.sh from a terminal
- Add prompt_user_config() that asks storage backend (sqlite/oceanbase),
  LLM provider (noop/anthropic/openai/...), and embedding provider (local/cloud)
- Wire into init.sh main flow, right before create_env_file, so answers are
  consumed as POWERMEM_INIT_* env vars by the embedded Python config generator
- Enhance /init skill SKILL.md with explicit AskUserQuestion instructions:
  conditional 3-question flow, env-var mapping, and POWERMEM_NON_INTERACTIVE=1
  invocation template for the Claude Code plugin path

* fix(init): refine SKILL.md to skip Storage prompt when server is healthy

When the server is already running and .env has DATABASE_PROVIDER set,
show the current backend instead of asking the user to reconfigure.
Only ask the Storage question when config is genuinely missing.

* feat(plugin): add remote-init short-circuit and 0.0.0.0 default host

init.sh gains a remote-init short-circuit: when POWERMEM_INIT_BASE_URL
(or non-localhost POWERMEM_BASE_URL) is provided, it skips .env, uvx,
and PID management, writing only runtime.env and/or .mcp.json per
POWERMEM_INIT_CONNECTION_MODE (hook|mcp|both, default both).

Dashboard default host changes from 127.0.0.1 to 0.0.0.0, overridable
via POWERMEM_SERVER_HOST, so the dashboard is reachable across
containers / remote hosts by default.

status.sh becomes mode-aware: reads runtime.env (hook_url) and
${PLUGIN_ROOT}/.mcp.json (mcp_url) and reports mode=hook|mcp|both|none.
common.sh gains shared helpers (is_remote_url, is_healthy,
write_runtime_remote) used by both scripts.

* feat(init): AskUserQuestion flow with mode-aware status SKILL

init SKILL.md describes a 3-round AskUserQuestion flow (Storage →
LLM → Embedding) that replaces the shell-level interactive prompts.
Each question is asked only when the relevant config is missing,
and CLAUDE_PLUGIN_ROOT is resolved per-invocation rather than
baked into shell env. When the server is already healthy with a
configured DATABASE_PROVIDER, the Storage question is skipped.

status SKILL.md documents the new hook|mcp|both|none mode reporting
and how to read runtime.env / .mcp.json.

* refactor(embeddings): extract shared model-cache helpers

Move is_model_cached, detect_country, download_via_modelscope, and
bridge_modelscope_to_hf_cache out of pyseekdb_default.py into a new
_model_cache.py module so both pyseekdb_default and huggingface can
share the same CN-mirror download / cache-bridge logic without
duplication. Tests are updated to patch _model_cache instead of
pyseekdb_default internals.

* fix(init): make 0.0.0.0 opt-in via Question 0, restore loopback default

Restore 127.0.0.1 as the script default for POWERMEM_SERVER_HOST so
loopback remains the local exposure model. All-interface binding
(0.0.0.0) becomes an explicit user choice: Question 0 of the
AskUserQuestion flow now splits "Local" into two options —
loopback-only and all-interfaces — and maps the answer to
POWERMEM_SERVER_HOST before invoking init.sh.

Update the uvx-launcher test to assert the new parameterized form
`--host "${POWERMEM_SERVER_HOST:-127.0.0.1}"` instead of the old
hardcoded `--host 127.0.0.1`. Addresses PR oceanbase#1101 review feedback.

* fix(init): write remote MCP config to user-scope, not plugin cache

Remote MCP mode was writing to $PLUGIN_ROOT/.mcp.json (the plugin cache
dir), which is volatile — wiped on every plugin uninstall+install — so
the user's remote MCP config was lost on reinstalls. It also could not
take effect without a plugin reload.

Register the powermem MCP server in the user-scope config instead, via
the `claude mcp add --scope user --transport http` CLI. The CLI decides
where to store it (typically ~/.claude.json top-level mcpServers) so the
location tracks the current Claude Code version / platform rather than
being hardcoded. Hook-only mode runs `claude mcp remove powermem
--scope user` to disable MCP, preserving any other MCP servers.

Adds write_user_mcp_config / remove_user_mcp_config helpers in common.sh
that wrap the CLI with a PATH check and idempotent remove-then-add.

* fix(init): address PR oceanbase#1101 review — quote runtime.env, disable hook in MCP mode

Four review issues on the user-scope MCP config commit (9f5ac95):

1. write_runtime_remote wrote URLs/keys bare, so shell metacharacters
   ($, ;, spaces, backticks, quotes) broke sourcing by run-hook.sh /
   status.sh. Switch to single-quoted values with '\'' escaping.

2. status.sh read $PLUGIN_ROOT/.mcp.json for the MCP URL, but that file is
   the volatile plugin-cache config (always empty since 9f5ac95 moved
   writes to user-scope). Read ~/.claude.json top-level mcpServers.powermem.url
   instead — the file the `claude mcp` CLI actually writes to.

3. MCP-only mode wrote nothing to runtime.env, leaving stale
   POWERMEM_BASE_URL from a prior hook/both init. The hook binary then
   ran against the old URL. Now write_runtime_hook_disabled() emits
   POWERMEM_HOOK_DISABLED=1, and run-hook.sh checks that marker after
   loading runtime.env and exits 0 before exec'ing the native binary.

4. common.sh had trailing blank lines at EOF (git diff --check).

Also tightens Round 1 remote follow-up AskUserQuestion wording in
SKILL.md: dedup the two URL placeholder options into distinct HTTP/HTTPS
protocol hints; clarify Hook mode works when auth is disabled or a key
was provided. Both connection mode retained per user request.

Adds regression tests for the quoting, hook-disabled marker, run-hook
early exit, and init.sh mcp-branch wiring.

* fix(init): drop "None" embedding option for OceanBase

OceanBase requires a vector embedding field to store memories, so
embeddings are mandatory. Offering "None" in the AskUserQuestion Round 3
options for the OceanBase storage path would let users pick an invalid
combination. SQLite keeps "None" since it can fall back to non-vector
storage; OceanBase only offers "Built-in seekdb" and "Cloud (<provider>)".

Also clarifies the mapping line: POWERMEM_INIT_EMBEDDING_PROVIDER=none is
now annotated "SQLite only".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants