Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

509 lines
18 KiB
Python

"""Slash commands for /integrations and /mcp."""
from __future__ import annotations
from rich.console import Console
from rich.markup import escape
import surfaces.interactive_shell.command_registry.repl_data as repl_data
from core.agent_harness.session.terminal_access import session_terminal
from surfaces.interactive_shell.command_registry.cli_parity import (
publish_headless_slash_response,
run_cli_command,
)
from surfaces.interactive_shell.command_registry.types import SlashCommand
from surfaces.interactive_shell.runtime import Session
from surfaces.interactive_shell.ui import (
BOLD_BRAND,
DIM,
ERROR,
HIGHLIGHT,
MCP_INTEGRATION_SERVICES,
WARNING,
render_integrations_table,
render_mcp_table,
repl_table,
)
from surfaces.interactive_shell.ui.components.choice_menu import (
CRUMB_SEP,
prepare_repl_output_line,
repl_choose_one,
repl_section_break,
repl_tty_interactive,
)
from surfaces.interactive_shell.ui.components.rendering import (
_repl_table_width,
print_repl_table,
repl_print,
)
_ROOT_INTEGRATIONS = "/integrations"
_ROOT_MCP = "/mcp"
_MAX_OBSERVATION_DETAIL_CHARS = 160
def _record_integrations_observation(session: Session, results: list[dict[str, str]]) -> None:
"""Stash a compact text view of verification results for agent summarization.
Lets the agent answer questions like "is sentry installed?" by summarizing
what ``/integrations`` actually found, instead of leaving the user with only
a raw table. Kept plain-text and bounded so it is cheap to feed back to the
assistant.
"""
lines: list[str] = []
for record in results:
service = str(record.get("service", "")).strip()
if not service:
continue
status = str(record.get("status", "")).strip() or "unknown"
detail = str(record.get("detail", "")).strip()
if len(detail) > _MAX_OBSERVATION_DETAIL_CHARS:
detail = f"{detail[: _MAX_OBSERVATION_DETAIL_CHARS - 1]}…"
line = f"- {service}: {status}"
if detail:
line += f" ({detail})"
lines.append(line)
if lines:
session.agent.last_observation = "Integration status from `/integrations`:\n" + "\n".join(
lines
)
def _record_integration_show_observation(session: Session, match: dict[str, str]) -> None:
"""Stash a compact text view of a single integration's verified details."""
lines: list[str] = []
for key, value in match.items():
text = str(value).strip()
if len(text) > _MAX_OBSERVATION_DETAIL_CHARS:
text = f"{text[: _MAX_OBSERVATION_DETAIL_CHARS - 1]}…"
lines.append(f"- {key}: {text}")
if lines:
session.agent.last_observation = (
"Integration detail from `/integrations show`:\n" + "\n".join(lines)
)
def _configured_service_choices() -> list[tuple[str, str]]:
"""Build picker choices from configured integrations (no live verification)."""
return [(name, name) for name in repl_data.configured_integration_names()]
def _handle_remove(session: Session, console: Console, service: str | None) -> bool:
"""Remove an integration with a native inline-picker confirmation (no subprocess)."""
from integrations.registry import resolve_management_service
from integrations.store import remove_integration
from platform.analytics.cli import capture_integration_removed
svc = resolve_management_service(service) if service else service
if not svc:
if not repl_tty_interactive():
repl_print(console, f"[{DIM}]usage:[/] /integrations remove <service>")
session.mark_latest(ok=False, kind="slash")
return True
choices = _configured_service_choices()
if not choices:
repl_print(console, f"[{DIM}]no integrations in store to remove.[/]")
return True
svc = repl_choose_one(
title="select integration to remove",
breadcrumb=f"{_ROOT_INTEGRATIONS}{CRUMB_SEP}remove",
choices=choices,
)
if not svc:
return True
if repl_tty_interactive():
confirmed = repl_choose_one(
title=f"remove '{escape(svc)}'?",
breadcrumb=f"{_ROOT_INTEGRATIONS}{CRUMB_SEP}remove{CRUMB_SEP}{escape(svc)}",
choices=[
("no", "No, cancel"),
("yes", f"Yes, remove '{svc}'"),
],
)
prepare_repl_output_line()
if confirmed != "yes":
repl_print(console, f"[{DIM}]cancelled.[/]")
session.refresh_integration_state()
return True
else:
import sys
try:
import questionary
confirmed_bool = questionary.confirm(f" Remove '{svc}'?", default=False).ask()
except (EOFError, KeyboardInterrupt):
session.refresh_integration_state()
return True
if not confirmed_bool:
print(" Cancelled.", file=sys.stderr)
session.refresh_integration_state()
return True
if remove_integration(svc):
repl_print(console, f"[{HIGHLIGHT}]removed '{escape(svc)}'.[/]")
capture_integration_removed(svc)
if svc == "github":
from surfaces.interactive_shell.runtime.startup.first_launch_github import (
clear_github_login_deferral,
)
clear_github_login_deferral()
else:
repl_print(console, f"[{ERROR}]no integration found for:[/] {escape(svc)}")
session.mark_latest(ok=False, kind="slash")
session.refresh_integration_state()
return True
def _mcp_service_choices() -> list[tuple[str, str]]:
names = [
name
for name in repl_data.configured_integration_names()
if name in MCP_INTEGRATION_SERVICES
]
return [(name, name) for name in names]
def _print_verify_summary(
console: Console, results: list[dict[str, str]], *, single_service: bool
) -> None:
failed = [r for r in results if r.get("status") in ("failed", "missing")]
if single_service:
if not results:
return
service = escape(str(results[0].get("service", "?")))
style = WARNING if failed else HIGHLIGHT
detail = "needs attention" if failed else "ok"
repl_print(console, f"[{style}]{service} {detail}.[/]")
return
if failed:
repl_print(console, f"[{WARNING}]{len(failed)} integration(s) need attention.[/]")
else:
repl_print(console, f"[{HIGHLIGHT}]all integrations ok.[/]")
def _run_verify(session: Session, console: Console, service: str | None = None) -> bool:
normalized = ""
if service is not None:
from integrations.registry import SUPPORTED_VERIFY_SERVICES, resolve_management_service
normalized = resolve_management_service(service)
if normalized not in SUPPORTED_VERIFY_SERVICES:
repl_print(
console,
f"[{ERROR}]unsupported verify target:[/] {escape(normalized)} "
f"(try [bold]/verify[/bold] with no args to verify all)",
)
session.mark_latest(ok=False, kind="slash")
return True
prepare_repl_output_line()
label = escape(normalized) if service is not None else "integrations"
with console.status(f"[{DIM}]Verifying {label}…[/]", spinner="dots"):
if service is not None:
match = repl_data.verify_integration(normalized)
if match is None:
repl_print(console, f"[{ERROR}]service not found:[/] {escape(normalized)}")
session.mark_latest(ok=False, kind="slash")
return True
results = [match]
else:
results = repl_data.load_verified_integrations()
_record_integrations_observation(session, results)
render_integrations_table(console, results)
_print_verify_summary(console, results, single_service=service is not None)
return True
def _cmd_verify(session: Session, console: Console, args: list[str]) -> bool:
return _cmd_integrations(session, console, ["verify", *args])
def _render_integration_show(session: Session, console: Console, service: str) -> bool:
"""Verify and print one integration. Returns False when the service is unknown."""
from integrations.registry import resolve_management_service
normalized = resolve_management_service(service)
configured = set(repl_data.configured_integration_names())
if normalized not in configured:
repl_print(console, f"[{ERROR}]service not found:[/] {escape(normalized)}")
return False
prepare_repl_output_line()
with console.status(
f"[{DIM}]Verifying {escape(normalized)}…[/]",
spinner="dots",
):
match = repl_data.verify_integration(normalized)
if match is None:
repl_print(console, f"[{ERROR}]service not found:[/] {escape(normalized)}")
return False
_record_integration_show_observation(session, match)
width = _repl_table_width(console)
table = repl_table(
title=f"Integration: {normalized}",
title_style=BOLD_BRAND,
show_header=False,
width=width,
)
table.add_column("key", style="bold", no_wrap=True)
value_width = max(20, width - 20)
table.add_column("value", overflow="fold", max_width=value_width)
for key, value in match.items():
table.add_row(escape(key), escape(str(value)))
print_repl_table(console, table)
return True
def _run_integrations_setup(session: Session, console: Console, args: list[str]) -> bool:
headless = session_terminal(session) is None
if len(args) < 2:
# Bare setup delegates to the CLI service picker on the REPL; headless
# surfaces (Telegram) have no picker, so return usage guidance instead.
if not headless:
result = run_cli_command(console, ["integrations", "setup"])
session.refresh_integration_state()
return result
repl_print(console, f"[{DIM}]usage:[/] /integrations setup <service>")
publish_headless_slash_response(
session, message="Usage: /integrations setup <service>", ok=False
)
return True
service = args[1]
cli_cmd = " ".join(["uv run opensre integrations setup", service, *args[2:]]).strip()
if headless:
message = (
f"{escape(service)} setup needs interactive credentials (API keys, URLs, tokens) "
f"and cannot finish in Telegram.\n\n"
f"Run on the server:\n {cli_cmd}\n\n"
"Then check status with `/integrations list` or "
f"`/integrations verify {escape(service)}`."
)
repl_print(console, message)
publish_headless_slash_response(session, message=message, ok=True)
session.refresh_integration_state()
return True
result = run_cli_command(
console,
["integrations", "setup", service, *args[2:]],
session=session,
)
session.refresh_integration_state()
return result
def _cmd_integrations(session: Session, console: Console, args: list[str]) -> bool:
if not args and repl_tty_interactive():
return _interactive_integrations_menu(session, console)
sub = (args[0].lower() if args else "list").strip()
if sub in ("list", "ls"):
prepare_repl_output_line()
with console.status(f"[{DIM}]Verifying integrations…[/]", spinner="dots"):
results = repl_data.load_verified_integrations()
_record_integrations_observation(session, results)
render_integrations_table(console, results)
return True
if sub == "verify":
if len(args) > 2:
repl_print(
console,
f"[{DIM}]usage:[/] /integrations verify [service] "
f"(or [bold]/verify [service][/bold])",
)
session.mark_latest(ok=False, kind="slash")
return True
return _run_verify(session, console, args[1] if len(args) == 2 else None)
if sub == "setup":
return _run_integrations_setup(session, console, args)
if sub == "remove":
return _handle_remove(session, console, args[1] if len(args) > 1 else None)
if sub == "show":
if len(args) < 2:
repl_print(console, f"[{DIM}]usage:[/] /integrations show <service>")
session.mark_latest(ok=False, kind="slash")
return True
if not _render_integration_show(session, console, args[1]):
session.mark_latest(ok=False, kind="slash")
return True
repl_print(
console,
f"[{ERROR}]unknown subcommand:[/] {escape(sub)} "
"(try [bold]/integrations list[/bold], [bold]/integrations verify[/bold], "
"or [bold]/integrations show <service>[/bold])",
)
session.mark_latest(ok=False, kind="slash")
return True
def _interactive_integrations_menu(session: Session, console: Console) -> bool:
root = _ROOT_INTEGRATIONS
while True:
sub = repl_choose_one(
title="integrations",
breadcrumb=root,
choices=[
("list", "/integrations list"),
("verify", "/integrations verify"),
("show", "/integrations show <service>"),
("setup", "/integrations setup <service>"),
("remove", "/integrations remove <service>"),
("done", "done"),
],
)
if sub is None or sub == "done":
return True
show_section_break = False
if sub == "list":
_cmd_integrations(session, console, ["list"])
show_section_break = True
elif sub == "verify":
_cmd_integrations(session, console, ["verify"])
show_section_break = True
elif sub == "setup":
_cmd_integrations(session, console, ["setup"])
show_section_break = True
elif sub == "show":
choices = _configured_service_choices()
if not choices:
repl_print(console, f"[{DIM}]no integrations in store to show.[/]")
show_section_break = True
else:
svc = repl_choose_one(
title="service",
breadcrumb=f"{root}{CRUMB_SEP}show",
choices=choices,
)
if svc and _render_integration_show(session, console, svc):
show_section_break = True
elif sub == "remove":
_handle_remove(session, console, None)
show_section_break = True
if show_section_break:
repl_section_break(console)
def _cmd_mcp(session: Session, console: Console, args: list[str]) -> bool:
if not args and repl_tty_interactive():
return _interactive_mcp_menu(session, console)
sub = (args[0].lower() if args else "list").strip()
if sub in ("list", "ls"):
render_mcp_table(console, repl_data.load_verified_integrations())
return True
if sub == "connect":
return _run_integrations_setup(session, console, ["setup", *args[1:]])
if sub == "disconnect":
return _handle_remove(session, console, args[1] if len(args) > 1 else None)
console.print(
f"[{ERROR}]unknown subcommand:[/] {escape(sub)} "
"(try [bold]/mcp list[/bold], [bold]/mcp connect[/bold], or [bold]/mcp disconnect[/bold])"
)
return True
def _interactive_mcp_menu(session: Session, console: Console) -> bool:
root = _ROOT_MCP
while True:
sub = repl_choose_one(
title="mcp",
breadcrumb=root,
choices=[
("list", "/mcp list"),
("connect", "/mcp connect <server>"),
("disconnect", "/mcp disconnect <server>"),
("done", "done"),
],
)
if sub is None or sub == "done":
return True
show_section_break = False
if sub == "list":
_cmd_mcp(session, console, ["list"])
show_section_break = True
elif sub == "connect":
_cmd_mcp(session, console, ["connect"])
show_section_break = True
elif sub == "disconnect":
choices = _mcp_service_choices()
if not choices:
repl_print(console, f"[{DIM}]no MCP servers configured.[/]")
show_section_break = True
else:
svc = repl_choose_one(
title="server",
breadcrumb=f"{root}{CRUMB_SEP}disconnect",
choices=choices,
)
if svc:
_cmd_mcp(session, console, ["disconnect", svc])
show_section_break = True
if show_section_break:
repl_section_break(console)
_INTEGRATIONS_FIRST_ARGS: tuple[tuple[str, str], ...] = (
("list", "list all configured integrations"),
("ls", "alias for list"),
("verify", "run health checks on all integrations"),
("show", "show details for a single integration"),
)
_MCP_FIRST_ARGS: tuple[tuple[str, str], ...] = (
("list", "list connected MCP servers"),
("ls", "alias for list"),
("connect", "add an MCP server via opensre integrations setup"),
("disconnect", "remove an MCP server"),
)
COMMANDS: list[SlashCommand] = [
SlashCommand(
"/verify",
"Verify configured integration connectivity.",
_cmd_verify,
usage=("/verify", "/verify <service>"),
),
SlashCommand(
"/integrations",
"Manage integrations.",
_cmd_integrations,
usage=(
"/integrations",
"/integrations list",
"/integrations verify",
"/integrations verify <service>",
"/integrations show <service>",
),
notes=("In a TTY, bare /integrations opens an interactive menu.",),
first_arg_completions=_INTEGRATIONS_FIRST_ARGS,
),
SlashCommand(
"/mcp",
"Manage MCP servers.",
_cmd_mcp,
usage=("/mcp", "/mcp list", "/mcp connect", "/mcp disconnect"),
notes=("In a TTY, bare /mcp opens an interactive menu.",),
first_arg_completions=_MCP_FIRST_ARGS,
),
]
__all__ = ["COMMANDS"]