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
148 lines
5.3 KiB
Python
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")
|