Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
1419ead
refactor(integrations): remove TS-only openclaw plugin (replaced by o…
yilu331 May 19, 2026
3976c11
feat(openclaw): scaffold openclaw-smart plugin directory tree
yilu331 May 19, 2026
7646a83
feat(openclaw): add TS deps (package.json, tsconfig.json, types shim,…
yilu331 May 19, 2026
e929658
feat(openclaw): add Python deps via uv (pyproject.toml, uv.lock, READ…
yilu331 May 19, 2026
96cb4cd
feat(openclaw): add plugin manifest with config schema
yilu331 May 19, 2026
7427e06
feat(server/llm): add openclaw_provider LiteLLM CustomLLM (23 tests)
yilu331 May 19, 2026
a715a3d
feat(server/llm): register openclaw provider alongside claude_code at…
yilu331 May 19, 2026
7994b4d
feat(openclaw): port ids.py with agent_id fallback
yilu331 May 19, 2026
f3183fd
feat(openclaw): port internal_call.py recursion guard
yilu331 May 19, 2026
2f1e432
feat(openclaw): port runtime.py with HOST_OPENCLAW
yilu331 May 19, 2026
f75b31c
feat(openclaw): port state.py JSONL buffer
yilu331 May 19, 2026
93e0183
feat(openclaw): port query_compose.py
yilu331 May 19, 2026
d1de4db
feat(openclaw): port stall_banner.py
yilu331 May 19, 2026
a7d63bb
feat(openclaw): port reflexio_adapter.py with agent_version=openclaw
yilu331 May 19, 2026
9abeca0
feat(openclaw): port oc_cite.py with [oc:] marker prefix
yilu331 May 19, 2026
6a426bd
feat(openclaw): port context_format.py with [oc:] marker prefix
yilu331 May 19, 2026
f01fbfc
feat(openclaw): port context_inject.py pipeline
yilu331 May 19, 2026
4802d69
feat(openclaw): port publish.py
yilu331 May 19, 2026
0dfd9e4
feat(openclaw): port optimizer_assistant.py
yilu331 May 19, 2026
9da209b
feat(openclaw/events): port session_start handler
yilu331 May 19, 2026
bca7817
feat(openclaw/events): port before_prompt_build handler
yilu331 May 19, 2026
014de84
feat(openclaw/events): port before_tool_call as observe-only stub
yilu331 May 19, 2026
64caf92
feat(openclaw/events): port after_tool_call handler
yilu331 May 19, 2026
57a7a1f
feat(openclaw/events): port agent_end handler
yilu331 May 19, 2026
51d0b45
feat(openclaw/events): port session_end handler with force_extraction…
yilu331 May 19, 2026
67ce4c5
feat(openclaw): port hook.py dispatcher with openclaw event map
yilu331 May 19, 2026
5a59e06
feat(openclaw): port cli.py command handlers
yilu331 May 19, 2026
3cb3cde
feat(openclaw/scripts): port _lib.sh shared utilities
yilu331 May 19, 2026
fe43e6e
feat(openclaw/scripts): port ensure-plugin-root.sh
yilu331 May 19, 2026
6b81163
feat(openclaw/scripts): port hook_entry.sh dispatch
yilu331 May 19, 2026
7e760aa
feat(openclaw/scripts): port cli.sh skill wrapper
yilu331 May 19, 2026
1d070e0
feat(openclaw/scripts): port backend-service.sh (port 8081)
yilu331 May 19, 2026
9820d4d
feat(openclaw/scripts): port backend-log-runner.sh
yilu331 May 19, 2026
c4a7ece
feat(openclaw/scripts): port dashboard-open.sh (shared dashboard with…
yilu331 May 19, 2026
815693d
feat(openclaw/scripts): port smart-install.sh first-run setup
yilu331 May 19, 2026
0297bcf
feat(openclaw): TS shim + vitest tests for hook forwarding
yilu331 May 19, 2026
7f4086a
feat(openclaw/skills): add 6 SKILL.md files (reflexio, learn, show, d…
yilu331 May 19, 2026
82749de
feat(cli/setup): rewrite 'reflexio setup openclaw' for openclaw-smart
yilu331 May 19, 2026
61b6f24
test(openclaw): integration test for recursion guard
yilu331 May 19, 2026
4dc0cfc
test(server/llm): integration test for openclaw_provider via LiteLLM
yilu331 May 19, 2026
f45150f
test(openclaw): e2e session loop test + TS shim payload flattening
yilu331 May 19, 2026
b11eece
test(openclaw): integration test for publish-to-reflexio
yilu331 May 19, 2026
42fe257
test(openclaw): integration test for search-and-inject
yilu331 May 19, 2026
d7f932e
docs(openclaw): rewrite README for openclaw-smart architecture
yilu331 May 19, 2026
f5bb3c7
style(server/llm): clean up ruff findings in openclaw_provider
yilu331 May 19, 2026
0e823e7
feat(openclaw): add tsc build step for openClaw plugin loader
yilu331 May 19, 2026
5cce131
fix(openclaw): address PR #80 code review findings
yilu331 May 19, 2026
c2ecd43
fix(openclaw): align default backend port with claude-smart (8071/8072)
yilu331 May 19, 2026
13ac704
fix(openclaw): drop reflexio_publish tool + use child_process.spawn
yilu331 May 19, 2026
9b7f244
fix(openclaw): activation.onStartup + allowConversationAccess gate hooks
yilu331 May 19, 2026
749ca69
fix(openclaw): real Telegram round-trip now persists JSONL + publishes
yilu331 May 19, 2026
f72e4fc
fix(openclaw): address PR review follow-ups
yilu331 May 21, 2026
0aaff89
feat(openclaw): add npm install surface
yilu331 May 21, 2026
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
255 changes: 207 additions & 48 deletions reflexio/cli/commands/setup_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,66 +570,162 @@ def _prompt_storage(env_path: Path) -> str:
raise typer.Exit(1)


def _install_openclaw_integration() -> bool:
"""Install the Reflexio plugin into OpenClaw via the plugin system.
_OPENCLAW_PLUGIN_ID = "reflexio-openclaw-smart"


def _openclaw_plugin_dir() -> Path:
"""Resolve the absolute path of the bundled openclaw-smart plugin dir."""
import reflexio

pkg_dir = Path(reflexio.__file__).parent
return pkg_dir / "integrations" / "openclaw" / "plugin"


def _write_openclaw_env(env_path: Path, openclaw_bin: str) -> None:
"""Persist the openclaw CLI path and local-CLI opt-in to ``~/.reflexio/.env``."""
_set_env_var(env_path, "OPENCLAW_BIN", openclaw_bin)
_set_env_var(env_path, "OPENCLAW_SMART_USE_LOCAL_CLI", "1")


def _remove_env_keys(env_path: Path, keys: tuple[str, ...]) -> None:
"""Delete the given keys from a ``.env`` file. Silent on missing file/keys."""
if not env_path.exists():
return
lines = env_path.read_text().splitlines()
drop = {f"{k}=" for k in keys}
kept = [line for line in lines if not any(line.startswith(p) for p in drop)]
if len(kept) != len(lines):
env_path.write_text("\n".join(kept) + ("\n" if kept else ""))


def _read_env_key(env_path: Path | None, key: str) -> str | None:
"""Read a simple KEY=value assignment from a .env file."""
if env_path is None or not env_path.exists():
return None
prefix = f"{key}="
for line in env_path.read_text().splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#") or not stripped.startswith(prefix):
continue
return stripped[len(prefix) :].strip().strip('"').strip("'") or None
return None


def _run_smart_install(plugin_dir: Path) -> None:
"""Run the plugin's first-run installer so uv/.venv land before first hook."""
script = plugin_dir / "scripts" / "smart-install.sh"
if not script.exists():
typer.echo(f"Warning: {script} missing, skipping first-run install")
return
proc = subprocess.run(["bash", str(script)], capture_output=True, text=True)
if proc.returncode != 0:
typer.echo(
"Warning: smart-install.sh exited "
f"{proc.returncode}; first session may take longer to bootstrap"
)
if proc.stderr:
typer.echo(proc.stderr.strip())


def _install_openclaw_integration(env_path: Path) -> bool:
"""Install the openclaw-smart plugin into openClaw via the plugin system.

Writes ``OPENCLAW_BIN`` and ``OPENCLAW_SMART_USE_LOCAL_CLI=1`` to
``~/.reflexio/.env``, installs and enables the bundled plugin, runs the
first-run installer, and verifies the plugin is loaded.

Args:
env_path (Path): Path to the ``~/.reflexio/.env`` file to update.

Returns:
bool: True if the plugin was verified as registered.

Raises:
typer.Exit: If the openclaw CLI is not found on PATH.
"""
if not shutil.which("openclaw"):
openclaw_bin = shutil.which("openclaw")
if not openclaw_bin:
typer.echo("Error: openclaw CLI not found. Install from https://openclaw.ai")
raise typer.Exit(1)

import reflexio

pkg_dir = Path(reflexio.__file__).parent
plugin_dir = pkg_dir / "integrations" / "openclaw" / "plugin"

plugin_dir = _openclaw_plugin_dir()
if not plugin_dir.exists():
typer.echo(f"Error: plugin directory not found at {plugin_dir}")
raise typer.Exit(1)

_write_openclaw_env(env_path, openclaw_bin)

# Use the resolved absolute path for every subprocess invocation so a
# mid-setup PATH change (a sibling install, a shell rc reload) can't
# silently flip us to a different ``openclaw`` binary partway through.
# Mirrors the same lookup discipline the runtime hooks use via OPENCLAW_BIN.
cli = openclaw_bin

# Clean install: remove any existing installation and stale extension dir
subprocess.run(
["openclaw", "plugins", "uninstall", "--force", "reflexio-federated"],
[cli, "plugins", "uninstall", "--force", _OPENCLAW_PLUGIN_ID],
check=False,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
capture_output=True,
text=True,
)
stale_ext = Path.home() / ".openclaw" / "extensions" / "reflexio-federated"
stale_ext = Path.home() / ".openclaw" / "extensions" / _OPENCLAW_PLUGIN_ID
shutil.rmtree(stale_ext, ignore_errors=True)

# Install plugin and restart gateway so inspect sees the new state
try:
subprocess.run(
["openclaw", "plugins", "install", str(plugin_dir)],
[cli, "plugins", "install", str(plugin_dir)],
check=True,
capture_output=True,
text=True,
)
subprocess.run(
["openclaw", "plugins", "enable", "reflexio-federated"],
[cli, "plugins", "enable", _OPENCLAW_PLUGIN_ID],
check=True,
capture_output=True,
text=True,
)
subprocess.run(
["openclaw", "gateway", "restart"],
check=True,
# Conversation-access opt-in is required for non-bundled plugins to
# receive typed hooks (agent_end, before_prompt_build, etc.). Without
# this, the gateway silently drops every plugin-side hook dispatch
# with: '[plugins] typed hook "agent_end" blocked because non-bundled
# plugins must set ...hooks.allowConversationAccess=true'.
access_cfg = subprocess.run(
[
cli,
"config",
"set",
f"plugins.entries.{_OPENCLAW_PLUGIN_ID}.hooks.allowConversationAccess",
"true",
],
check=False,
capture_output=True,
text=True,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if access_cfg.returncode != 0:
typer.echo(
"Error: could not persist openClaw conversation-access permission: "
f"{access_cfg.stderr or access_cfg.stdout}"
)
raise typer.Exit(1)
except subprocess.CalledProcessError as exc:
typer.echo(f"Error: openclaw command failed: {exc.stderr or exc.stdout}")
raise typer.Exit(1) from exc

# First-run: warm uv venv so the next hook isn't billed for cold install
_run_smart_install(plugin_dir)

# Gateway restart is best-effort — older openclaw builds may not expose it
subprocess.run(
[cli, "gateway", "restart"],
check=False,
capture_output=True,
text=True,
)

# Verify — match exact "Status: loaded" to avoid false positives from
# "not loaded" or "unloaded"
result = subprocess.run(
["openclaw", "plugins", "inspect", "reflexio-federated"],
[cli, "plugins", "inspect", _OPENCLAW_PLUGIN_ID],
capture_output=True,
text=True,
)
Expand All @@ -638,32 +734,46 @@ def _install_openclaw_integration() -> bool:
return True

typer.echo(
"Error: Plugin not loaded -- check 'openclaw plugins inspect reflexio-federated'"
f"Error: Plugin not loaded -- check 'openclaw plugins inspect {_OPENCLAW_PLUGIN_ID}'"
)
return False


def _uninstall_openclaw() -> None:
"""Remove the Reflexio integration from OpenClaw."""
def _uninstall_openclaw(env_path: Path | None = None, purge: bool = False) -> None:
"""Remove the openclaw-smart integration from openClaw.

Args:
env_path (Path | None): Path to ``~/.reflexio/.env`` to strip the
``OPENCLAW_BIN`` / ``OPENCLAW_SMART_USE_LOCAL_CLI`` keys from. ``None``
skips the .env cleanup.
purge (bool): Also delete ``~/.openclaw-smart/`` state directory.
"""
typer.confirm(
"This will remove the Reflexio integration from OpenClaw. Continue?",
"This will remove the Reflexio integration from openClaw. Continue?",
abort=True,
)
if shutil.which("openclaw"):
cli = _read_env_key(env_path, "OPENCLAW_BIN") or shutil.which("openclaw")
if cli:
subprocess.run(
["openclaw", "plugins", "disable", "reflexio-federated"],
[cli, "plugins", "disable", _OPENCLAW_PLUGIN_ID],
check=False,
capture_output=True,
text=True,
)
subprocess.run(
["openclaw", "plugins", "uninstall", "--force", "reflexio-federated"],
[cli, "plugins", "uninstall", "--force", _OPENCLAW_PLUGIN_ID],
check=False,
capture_output=True,
text=True,
)
else:
typer.echo("Warning: openclaw CLI not found on PATH, skipping plugin removal")
typer.echo(
"Warning: openclaw CLI not found in OPENCLAW_BIN or PATH, "
"skipping plugin removal"
)

if env_path is not None:
_remove_env_keys(env_path, ("OPENCLAW_BIN", "OPENCLAW_SMART_USE_LOCAL_CLI"))

# Remove setup markers
from reflexio.cli.paths import reflexio_home
Expand All @@ -674,15 +784,55 @@ def _uninstall_openclaw() -> None:
marker.unlink(missing_ok=True)
typer.echo(f"Removed setup marker: {marker}")

typer.echo("Reflexio integration fully removed from OpenClaw.")
if purge:
state_dir = Path.home() / ".openclaw-smart"
if state_dir.exists():
shutil.rmtree(state_dir, ignore_errors=True)
typer.echo(f"Removed plugin state: {state_dir}")

typer.echo("Reflexio integration fully removed from openClaw.")


def _repair_openclaw() -> None:
"""Re-run the plugin's first-run installer and clear any failure marker."""
plugin_dir = _openclaw_plugin_dir()
if not plugin_dir.exists():
typer.echo(f"Error: plugin directory not found at {plugin_dir}")
raise typer.Exit(1)
failure_marker = Path.home() / ".openclaw-smart" / "install-failed"
if failure_marker.exists():
failure_marker.unlink()
typer.echo(f"Cleared {failure_marker}")
_run_smart_install(plugin_dir)
typer.echo("Repair complete. Restart openClaw to pick up the refreshed env.")


@app.command("openclaw")
def openclaw(
uninstall: Annotated[
bool,
typer.Option(
"--uninstall", help="Remove the Reflexio integration from OpenClaw"
"--uninstall", help="Remove the Reflexio integration from openClaw"
),
] = False,
repair: Annotated[
bool,
typer.Option(
"--repair",
help=(
"Re-run the plugin first-run installer and clear any failure "
"marker without reinstalling the plugin in openClaw."
),
),
] = False,
purge: Annotated[
bool,
typer.Option(
"--purge",
help=(
"With --uninstall: also delete ~/.openclaw-smart/ state "
"(buffered sessions, install markers, logs). Destructive."
),
),
] = False,
embedding: Annotated[
Expand All @@ -697,9 +847,28 @@ def openclaw(
),
] = "auto",
) -> None:
"""Set up (or remove) the Reflexio integration for OpenClaw."""
"""Set up (or remove) the openclaw-smart integration for openClaw."""
# Resolve the user-global .env path up front; all three flows write to it.
from reflexio.cli.env_loader import ensure_user_env_for_setup

env_path = ensure_user_env_for_setup()
if env_path is None:
typer.echo("Error: could not locate or create a .env file")
raise typer.Exit(1)

if repair and (uninstall or purge):
typer.echo("Error: --repair cannot be combined with --uninstall or --purge")
raise typer.Exit(1)
if purge and not uninstall:
typer.echo("Error: --purge requires --uninstall")
raise typer.Exit(1)

if repair:
_repair_openclaw()
return

if uninstall:
_uninstall_openclaw()
_uninstall_openclaw(env_path=env_path, purge=purge)
return
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if embedding not in _VALID_EMBEDDING_FLAGS:
Expand All @@ -709,26 +878,15 @@ def openclaw(
)
raise typer.Exit(1)

# Step 1: Load .env path. Always target ~/.reflexio/.env — running setup
# from a worktree or project root that happens to contain its own .env
# would otherwise pollute that file via load_reflexio_env's CWD-first
# search. Setup writes are user-global, not project-local.
from reflexio.cli.env_loader import ensure_user_env_for_setup

env_path = ensure_user_env_for_setup()
if env_path is None:
typer.echo("Error: could not locate or create a .env file")
raise typer.Exit(1)

# Step 2: LLM provider
# Step 1: LLM provider
display_name, model, _provider_key = _prompt_llm_provider(env_path)

# Step 3: Storage. Decided BEFORE the embedding step because Managed /
# Step 2: Storage. Decided BEFORE the embedding step because Managed /
# Self-hosted modes own their own embedding config server-side, and a
# local override would just shadow the operator's choice.
storage_label = _prompt_storage(env_path)

# Step 3.5: Upfront embedding-provider step. Local is the default when
# Step 2.5: Upfront embedding-provider step. Local is the default when
# chromadb is importable; the choice persists to org config so it
# survives later cloud-key changes. Skipped for remote storage modes
# for the reason above.
Expand All @@ -737,12 +895,13 @@ def openclaw(
if not is_remote:
embedding_label = _choose_embedding_provider(env_path, embedding_flag=embedding)

# Step 4: Install OpenClaw integration
# Step 3: Install openclaw-smart integration
typer.echo("")
hook_ok = _install_openclaw_integration()
hook_ok = _install_openclaw_integration(env_path)

# Step 5: Summary
hook_status = "reflexio-federated" if hook_ok else "reflexio-federated (unverified)"
hook_status = (
_OPENCLAW_PLUGIN_ID if hook_ok else f"{_OPENCLAW_PLUGIN_ID} (unverified)"
)

typer.echo("")
typer.echo("Setup complete!")
Expand All @@ -753,7 +912,7 @@ def openclaw(
typer.echo(f" Plugin: {hook_status}")
typer.echo("")
typer.echo("Next steps:")
typer.echo(" 1. Restart OpenClaw gateway: openclaw gateway restart")
typer.echo(" 1. Restart openClaw gateway: openclaw gateway restart")
typer.echo(
" 2. Start a conversation -- Reflexio will capture and learn automatically"
)
Expand Down
Loading