Skip to content

CLI reference

Use the built-in help for the authoritative command list:

ursa --help

For subcommand-specific help:

ursa mcp-server --help
ursa exec --help
ursa rag-ingest --help
ursa rag-query --help
ursa self --help

Common top-level commands

URSA configs can be layered with environment variables and CLI flags.

ursa --config config.yaml
ursa --print-config
ursa --config config.yaml --name my-agent --group default
ursa --config config.yaml --use-web

Main subcommands

Current URSA installations include subcommands for:

  • running the MCP server,
  • managing groups,
  • managing persistent agents,
  • sharing/importing agents,
  • managing persistent RAG collections,
  • inspecting and managing the URSA installation,
  • non-interactive execution.

Use ursa --help to confirm the exact set in your installed version.

Self management

Installation status is available for every installation method:

ursa self status

It reports the URSA version and the running Python version and executable. For uv tool installations, it also reports the selected URSA extras and any additional packages installed in the tool environment.

When URSA was installed with uv tool install, it can update itself while preserving the installation recipe:

ursa self update

Use modify to change the recipe. Options can add extras or additional packages, select an exact release or Git ref, and clear the existing extras and additional packages:

ursa self modify --extra dashboard
ursa self modify --with some-package
ursa self modify --version 1.2.3
ursa self modify --ref main
ursa self modify --clean --extra fm

Run ursa self modify --help for the complete option list. For pip, Conda, and other installations, use the same package manager that installed URSA; self update and self modify will reject installations not managed by uv.

Python callback API

ursa.cli.callbacks.HITLLogEventHandler

Bases: CallbackRenderingMixin, AsyncCallbackHandler

Render structured agent/tool progress events to the REPL console.

Source code in src/ursa/cli/callbacks.py
class HITLLogEventHandler(CallbackRenderingMixin, AsyncCallbackHandler):
    """Render structured agent/tool progress events to the REPL console."""

    def __init__(self, console: Console, workspace: Path):
        self.console = console
        self.workspace = Path(workspace).resolve()
        self._last_agent: str | None = None
        self._inflight_tools: dict[Any, dict[str, Any]] = {}
        self.emitted_any = False

    @property
    def ignore_llm(self) -> bool:
        return True

    async def on_chat_model_start(
        self,
        serialized: Any,
        messages: Any,
        *,
        run_id,
        parent_run_id=None,
        tags=None,
        metadata=None,
        **kwargs,
    ) -> None:
        return None

    async def on_llm_start(
        self,
        serialized: Any,
        prompts: Any,
        *,
        run_id,
        parent_run_id=None,
        tags=None,
        metadata=None,
        **kwargs,
    ) -> None:
        return None

    async def on_tool_start(
        self,
        serialized: dict[str, Any],
        input_str: str,
        *,
        run_id,
        parent_run_id=None,
        tags=None,
        metadata=None,
        inputs: dict[str, Any] | None = None,
        **kwargs,
    ) -> None:
        tool_name = self._tool_name(serialized)
        payload = self._tool_input_payload(inputs, input_str)
        self._inflight_tools[run_id] = {
            "name": tool_name,
            "input": payload,
        }
        if tool_name == "run_command":
            self._print_run_command_start(payload.get("query"))
        elif tool_name == "read_file":
            self._print_read_file_start(
                payload.get("path") or payload.get("filename")
            )
        elif tool_name in {"write_code", "write_code_with_repo"}:
            self._print_write_code_start(
                payload.get("filename"),
                payload.get("path"),
                payload.get("code"),
            )
        elif tool_name in {"edit_code", "edit_experience"}:
            self._print_edit_code_start(
                payload.get("filename"),
                payload.get("path"),
                payload.get("old_code"),
                payload.get("new_code"),
            )

    async def on_tool_end(
        self,
        output: Any,
        *,
        run_id,
        parent_run_id=None,
        tags=None,
        **kwargs,
    ) -> None:
        tool_info = self._inflight_tools.pop(run_id, {})
        tool_name = tool_info.get("name")
        payload = output.content if isinstance(output, ToolMessage) else output
        success = (
            output.status == "success"
            if isinstance(output, ToolMessage)
            else True
        )
        if tool_name == "run_command":
            self._print_run_command_end(
                payload,
                query=tool_info.get("input", {}).get("query"),
                success=success,
            )
        elif tool_name == "read_file":
            self._print_read_file_end(
                payload,
                path=tool_info.get("input", {}).get("path")
                or tool_info.get("input", {}).get("filename"),
            )
        elif tool_name in {"write_code", "write_code_with_repo"}:
            self._print_write_code_end(
                payload,
                filename=tool_info.get("input", {}).get("filename"),
                path=tool_info.get("input", {}).get("path"),
                success=success,
            )
        elif tool_name == "edit_code":
            tool_input = tool_info.get("input", {})
            self._print_edit_code_end(
                payload,
                filename=tool_input.get("filename"),
                path=tool_input.get("path"),
                success=success,
            )

    async def on_tool_error(
        self,
        error: BaseException,
        *,
        run_id,
        parent_run_id=None,
        tags=None,
        **kwargs,
    ) -> None:
        tool_info = self._inflight_tools.pop(run_id, {})
        tool_name = tool_info.get("name", "tool")
        if not self._is_file_tool(tool_name) and tool_name != "run_command":
            return
        self.console.print(f"[error]✖ {tool_name} failed.[/]")
        self.emitted_any = True
        for line in self._display_lines(error):
            self.console.print(f"[dim]  {line}[/]")

    async def on_custom_event(
        self,
        name: str,
        data: Any,
        *,
        run_id,
        tags=None,
        metadata=None,
        **kwargs,
    ) -> None:
        if name != DEFAULT_EVENT_NAME or not isinstance(data, dict):
            return
        # Tool events may also carry an ``agent`` field identifying their
        # owner. Treat the primary tool discriminator as more specific than
        # that ownership metadata so named-agent artifacts are still rendered.
        if "tool" in data:
            tool = self._clean(data.get("tool"))
            self._print_event_artifact(data)
            if tool == "run_command":
                return
            if not self._is_rendered_tool(tool):
                return
            if tool == "read_file" and self._has_inflight_tool("read_file"):
                return
            if tool == "write_code" and self._has_inflight_tool(
                "write_code",
                "write_code_with_repo",
            ):
                return
            if tool == "edit_code" and self._has_inflight_tool("edit_code"):
                return
            self._print_tool_event(data)
        elif "agent" in data:
            self._print_agent_event(data)