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

148 lines
5.3 KiB
Python

"""Local agent fleet management CLI commands."""
from __future__ import annotations
import time
import click
from rich.console import Console
from rich.markup import escape
from platform.terminal.theme import BOLD_BRAND, DIM, HIGHLIGHT
from surfaces.interactive_shell.ui.components.rendering import repl_table
from tools.system.fleet_monitoring.discovery import (
classify_command_provider,
discover_agent_processes,
display_command,
process_command,
registered_and_discovered_agents,
)
from tools.system.fleet_monitoring.registry import AgentRecord, AgentRegistry
@click.group(name="fleet")
def fleet() -> None:
"""Manage the local AI agent fleet (Claude Code, Cursor, Aider, ...)."""
def _pid_exists(pid: int) -> bool:
try:
from tools.system.fleet_monitoring.probe import pid_exists
except ModuleNotFoundError as exc:
if exc.name != "psutil":
raise
raise click.ClickException(
"agent process watching requires psutil; run `uv sync` or reinstall OpenSRE"
) from exc
return pid_exists(pid)
@fleet.command(name="list")
def list_agents() -> None:
"""List registered and auto-discovered local agents."""
from surfaces.interactive_shell.ui.agents.agents_view import render_agents_table
console = Console()
render_agents_table(console, registered_and_discovered_agents(AgentRegistry()))
@fleet.command(name="register")
@click.argument("pid", type=int)
@click.argument("name", required=False)
@click.option("--command", "command_override", help="Command string to store for this agent.")
def register_agent(pid: int, name: str | None, command_override: str | None) -> None:
"""Start tracking a local agent process."""
if pid <= 0:
raise click.BadParameter("must be a positive integer", param_hint="pid")
agent_name = name or f"agent-{pid}"
command = command_override or process_command(pid) or agent_name
AgentRegistry().register(
AgentRecord(
name=agent_name,
pid=pid,
command=command,
provider=classify_command_provider(command),
)
)
click.echo(f"registered {agent_name} (pid {pid})")
@fleet.command(name="forget")
@click.argument("pid", type=int)
def forget_agent(pid: int) -> None:
"""Stop tracking a local agent process."""
removed = AgentRegistry().forget(pid)
if removed is None:
click.echo(f"no registered agent for pid {pid}")
return
click.echo(f"forgot {removed.name} (pid {pid})")
@fleet.command(name="scan")
@click.option("--register", "should_register", is_flag=True, help="Register all discovered agents.")
@click.option("--all", "include_all", is_flag=True, help="Include noisy helper processes.")
def scan_agents(should_register: bool, include_all: bool) -> None:
"""Discover running Cursor, Claude Code, Aider, and Codex sessions."""
console = Console(highlight=False)
candidates = discover_agent_processes(include_all=include_all)
if not candidates:
console.print(f"[{DIM}]no running AI-agent sessions detected[/]")
console.print(f"[{DIM}]use [bold]opensre fleet scan --all[/bold] to inspect helpers[/]")
return
table = repl_table(title="agent scan", title_style=BOLD_BRAND)
table.add_column("pid", justify="right", style=DIM, no_wrap=True)
table.add_column("agent", style="bold")
table.add_column("command", overflow="ellipsis", no_wrap=True, max_width=48)
registry = AgentRegistry()
for candidate in candidates:
table.add_row(
str(candidate.pid),
escape(candidate.name),
escape(display_command(candidate.command)),
)
if should_register:
registry.register(candidate.to_record())
console.print(table)
if should_register:
console.print(f"[{HIGHLIGHT}]registered {len(candidates)} agent(s)[/]")
elif include_all:
console.print(
f"[{DIM}]showing helper processes; use "
"[bold]opensre fleet scan[/bold] for likely agent sessions only[/]"
)
else:
console.print(
f"[{DIM}]Next: run [bold]opensre fleet scan --register[/bold] "
f"to track {len(candidates)} process(es)[/]"
)
@fleet.command(name="watch")
@click.argument("pid", type=int)
@click.option("--interval", default=1.0, show_default=True, help="Polling interval in seconds.")
@click.option("--timeout", type=float, default=None, help="Stop watching after this many seconds.")
def watch_agent(pid: int, interval: float, timeout: float | None) -> None:
"""Watch a PID until it exits and print a notification."""
if pid <= 0:
raise click.BadParameter("must be a positive integer", param_hint="pid")
if interval <= 0:
raise click.BadParameter("must be positive", param_hint="--interval")
if timeout is not None and timeout <= 0:
raise click.BadParameter("must be positive", param_hint="--timeout")
started = time.monotonic()
if not _pid_exists(pid):
click.echo(f"pid {pid} is not running")
return
click.echo(f"watching pid {pid}; press Ctrl+C to stop")
while _pid_exists(pid):
if timeout is not None and time.monotonic() - started >= timeout:
raise click.ClickException(f"pid {pid} is still running after {timeout:g}s")
time.sleep(interval)
click.echo(f"pid {pid} exited")