Skip to content

cli

Defines the CLI for mujoco-mojo.

SettingsScope

Bases: StrEnum

Which settings settings show should display.

EFFECTIVE class-attribute instance-attribute

Python
EFFECTIVE = 'effective'

The fully resolved, merged settings (project + global + env + defaults).

PROJECT class-attribute instance-attribute

Python
PROJECT = 'project'

The raw, unmerged contents of just the project-local settings file.

GLOBAL class-attribute instance-attribute

Python
GLOBAL = 'global'

The raw, unmerged contents of just the global settings file.

main

Python
main(
    ctx: Context,
    version: Annotated[
        bool,
        Option(
            "--version",
            callback=version_callback,
            is_eager=True,
            help="Show version and exit.",
        ),
    ] = False,
) -> None

[bold cyan]MuJoCo Mojo:[/bold cyan] High-performance and extensible physics simulation manager.

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@cli_app.callback(
    epilog="Check out the [link=https://hydrowelder.github.io/mujoco-mojo/]full documentation[/link] for more info."
)
def main(
    ctx: typer.Context,
    version: Annotated[
        bool,
        typer.Option(
            "--version",
            callback=version_callback,
            is_eager=True,
            help="Show version and exit.",
        ),
    ] = False,
) -> None:
    """
    [bold cyan]MuJoCo Mojo:[/bold cyan] High-performance and extensible physics simulation manager.
    """

run_monte_carlo

Python
run_monte_carlo(
    ctx: Context,
    generator: GeneratorType,
    runtime: RuntimeType = DEFAULT_RUNTIME,
    workdir: WorkdirType = DEFAULT_WORKDIR,
    n_trial: NTrialType = DEFAULT_MC_N_TRIAL,
    n_proc: NProcType = n_proc,
    resume: ResumeType = resume,
    seed: SeedType = DEFAULT_SEED,
    clean_workdir: CleanWorkdirType = clean_workdir,
    model_config_name: ModelConfigNameType = model_config_name,
    xml_name: XMLNameType = xml_name,
    execution_mode: ExecutionModeType = LOCAL,
    overrides: OverridesType = None,
    trial_nums: TrialNumsType = [],
    gen_args: GenArgsType = [],
    gen_kwargs: GenKwargsType = [],
    run_args: RunArgsType = [],
    run_kwargs: RunKwargsType = [],
    verbose: VerboseType = 0,
    quiet: QuietType = 0,
) -> None

[bold yellow]Execute a Monte Carlo campaign.[/bold yellow]

This command handles the directory setup, distribution salting, and parallel execution of physics trials.

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@run_app.command(name="monte-carlo")
def run_monte_carlo(
    ctx: typer.Context,
    generator: GeneratorType,
    runtime: RuntimeType = DEFAULT_RUNTIME,
    workdir: WorkdirType = DEFAULT_WORKDIR,
    n_trial: NTrialType = DEFAULT_MC_N_TRIAL,
    n_proc: NProcType = _SETTINGS.general.n_proc,
    resume: ResumeType = _SETTINGS.run.monte_carlo.resume,
    seed: SeedType = DEFAULT_SEED,
    clean_workdir: CleanWorkdirType = _SETTINGS.run.monte_carlo.clean_workdir,
    model_config_name: ModelConfigNameType = _SETTINGS.general.model_config_name,
    xml_name: XMLNameType = _SETTINGS.general.xml_name,
    execution_mode: ExecutionModeType = ExecutionMode.LOCAL,
    overrides: OverridesType = None,
    trial_nums: TrialNumsType = [],
    gen_args: GenArgsType = [],
    gen_kwargs: GenKwargsType = [],
    run_args: RunArgsType = [],
    run_kwargs: RunKwargsType = [],
    verbose: VerboseType = 0,
    quiet: QuietType = 0,
) -> None:
    """
    [bold yellow]Execute a Monte Carlo campaign.[/bold yellow]

    This command handles the directory setup, distribution salting, and parallel execution of physics trials.
    """
    from numpydantic import NDArray

    from mujoco_mojo.stochas import NamedValueDict
    from mujoco_mojo.utils.runner import MonteCarloConfig

    logger = _setup_cli_logging(
        verbose=_SETTINGS.general.verbose + verbose,
        quiet=_SETTINGS.general.quiet + quiet,
    )

    print_logo()

    workdir = workdir.resolve()

    logger.info("Initializing Monte Carlo with CLI!")

    dojo_cmd = f'mujoco-mojo dojo "{workdir}"'
    console.print(
        Panel(
            "[bold green]Campaign Initialized![/]\n\n"
            "[white]To monitor progress and view results, run:[/]\n"
            f"    [bold yellow]{dojo_cmd}[/]",
            title="[cyan]Monte Carlo Initialized[/]",
            expand=False,
            border_style="cyan",
        )
    )

    global_overrides = None
    if overrides:
        overrides = overrides.resolve()
        logger.info(f"Retrieving global NamedValue overrides from `{overrides}`")
        global_overrides = NamedValueDict[NDArray].model_validate_json(
            overrides.read_text()
        )

        if len(global_overrides) == 0:
            logger.warning(
                "Global NamedValue overrides had no entries. Continuing anyway."
            )
        else:
            logger.info(
                f"Global NamedValue overrides had {len(global_overrides)} entries."
            )

    if n_trial != 0 and trial_nums:
        logger.warning(
            "n-trial was not set to 0 with trial IDs provided. Setting n-trial to 0 and continuing."
        )
        n_trial = 0

    runner = _prepare_runner(
        generator=generator,
        runtime=runtime,
        workdir=workdir,
        objective=None,
        model_config_name=model_config_name,
        seed=seed,
        xml_name=xml_name,
        gen_args=gen_args,
        gen_kwargs=gen_kwargs,
        run_args=run_args,
        run_kwargs=run_kwargs,
    )

    # 2. build config
    runner.config = MonteCarloConfig(n_trial=n_trial, n_proc=n_proc, resume=resume)

    # 3. run
    console.print(
        f"[bold magenta]Starting {n_trial} trials[/bold magenta] (using {n_proc} workers)..."
    )
    logger.info(
        f"Starting {n_trial} trials (using {n_proc} workers)...",
        extra={"file_only": True},
    )
    had_fails = runner.run(
        global_overrides=global_overrides
        if global_overrides
        else NamedValueDict[NDArray](),
        clean_workdir=clean_workdir,
        execution_mode=execution_mode,
        trial_nums=trial_nums,
    )

    match execution_mode:
        case ExecutionMode.LOCAL:
            if had_fails:
                preamble = "[bold red]Monte Carlo finished with failures![/bold red]"
                logger.error(
                    f"Monte Carlo finished with failures! See results in {runner.workdir.resolve()}",
                    extra={"file_only": True},
                )
            else:
                preamble = "[bold green]Monte Carlo finished![/bold green]"
                logger.info(
                    f"Monte Carlo finished! See results in {runner.workdir.resolve()}",
                    extra={"file_only": True},
                )
            console.print(
                f"\n{preamble} Results located at [italic underline]{runner.workdir.resolve()}[/italic underline]"
            )
        case ExecutionMode.SLURM:
            if had_fails:
                finished_msg = (
                    "[bold red]Failed to orchestrate SLURM Monte Carlo![/bold red]"
                )
                logger.error(
                    "Failed to orchestrate SLURM Monte Carlo!",
                    extra={"file_only": True},
                )
            else:
                finished_msg = (
                    "[bold green]SLURM Monte Carlo orchestration finished![/bold green]"
                )
                logger.info(
                    "SLURM Monte Carlo orchestration finished!",
                    extra={"file_only": True},
                )
            console.print(f"\n{finished_msg}")

    raise typer.Exit(code=1 if had_fails else 0)

run_single

Python
run_single(
    ctx: Context,
    generator: GeneratorType,
    runtime: RuntimeType = DEFAULT_RUNTIME,
    workdir: WorkdirType = DEFAULT_WORKDIR,
    n_proc: NProcType = n_proc,
    resume: ResumeType = resume,
    seed: SeedType = DEFAULT_SEED,
    clean_workdir: CleanWorkdirType = clean_workdir,
    model_config_name: ModelConfigNameType = model_config_name,
    xml_name: XMLNameType = xml_name,
    execution_mode: ExecutionModeType = LOCAL,
    overrides: OverridesType = None,
    trial_num: TrialNumType = NOMINAL_TRIAL_NUM,
    gen_args: GenArgsType = [],
    gen_kwargs: GenKwargsType = [],
    run_args: RunArgsType = [],
    run_kwargs: RunKwargsType = [],
    verbose: VerboseType = 0,
    quiet: QuietType = 0,
) -> None

[bold yellow]Execute a single trial.[/bold yellow]

This command handles the directory setup, distribution salting, and execution of exactly one physics trial (trial 0, the nominal run, unless [bold cyan]--trial-num[/bold cyan] targets a different one).

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@run_app.command(name="single")
def run_single(
    ctx: typer.Context,
    generator: GeneratorType,
    runtime: RuntimeType = DEFAULT_RUNTIME,
    workdir: WorkdirType = DEFAULT_WORKDIR,
    n_proc: NProcType = _SETTINGS.general.n_proc,
    resume: ResumeType = _SETTINGS.run.single.resume,
    seed: SeedType = DEFAULT_SEED,
    clean_workdir: CleanWorkdirType = _SETTINGS.run.single.clean_workdir,
    model_config_name: ModelConfigNameType = _SETTINGS.general.model_config_name,
    xml_name: XMLNameType = _SETTINGS.general.xml_name,
    execution_mode: ExecutionModeType = ExecutionMode.LOCAL,
    overrides: OverridesType = None,
    trial_num: TrialNumType = NOMINAL_TRIAL_NUM,
    gen_args: GenArgsType = [],
    gen_kwargs: GenKwargsType = [],
    run_args: RunArgsType = [],
    run_kwargs: RunKwargsType = [],
    verbose: VerboseType = 0,
    quiet: QuietType = 0,
) -> None:
    """
    [bold yellow]Execute a single trial.[/bold yellow]

    This command handles the directory setup, distribution salting, and execution of exactly one physics trial (trial 0, the nominal run, unless [bold cyan]--trial-num[/bold cyan] targets a different one).
    """
    from numpydantic import NDArray

    from mujoco_mojo.stochas import NamedValueDict
    from mujoco_mojo.utils.runner import MonteCarloConfig

    logger = _setup_cli_logging(
        verbose=_SETTINGS.general.verbose + verbose,
        quiet=_SETTINGS.general.quiet + quiet,
    )

    print_logo()

    workdir = workdir.resolve()

    logger.info("Initializing single trial with CLI!")

    dojo_cmd = f'mujoco-mojo dojo "{workdir}"'
    console.print(
        Panel(
            "[bold green]Trial Ready![/]\n\n"
            "[white]To monitor progress and view results, run:[/]\n"
            f"    [bold yellow]{dojo_cmd}[/]",
            title="[cyan]Single Run Initialized[/]",
            expand=False,
            border_style="cyan",
        )
    )

    global_overrides = None
    if overrides:
        overrides = overrides.resolve()
        logger.info(f"Retrieving global NamedValue overrides from `{overrides}`")
        global_overrides = NamedValueDict[NDArray].model_validate_json(
            overrides.read_text()
        )

        if len(global_overrides) == 0:
            logger.warning(
                "Global NamedValue overrides had no entries. Continuing anyway."
            )
        else:
            logger.info(
                f"Global NamedValue overrides had {len(global_overrides)} entries."
            )

    runner = _prepare_runner(
        generator=generator,
        runtime=runtime,
        workdir=workdir,
        objective=None,
        model_config_name=model_config_name,
        seed=seed,
        xml_name=xml_name,
        gen_args=gen_args,
        gen_kwargs=gen_kwargs,
        run_args=run_args,
        run_kwargs=run_kwargs,
    )

    # a single trial is always just one trial - n_trial only exists on
    # MonteCarloConfig for padding-width bookkeeping, it doesn't affect which
    # trial actually runs (that's `trial_num`, passed explicitly below)
    runner.config = MonteCarloConfig(n_trial=1, n_proc=n_proc, resume=resume)

    console.print(f"[bold magenta]Running trial {trial_num}[/bold magenta]...")
    logger.info(f"Running trial {trial_num}...", extra={"file_only": True})

    had_fails = runner.run(
        global_overrides=global_overrides
        if global_overrides
        else NamedValueDict[NDArray](),
        clean_workdir=clean_workdir,
        execution_mode=execution_mode,
        trial_nums=[trial_num],
    )

    match execution_mode:
        case ExecutionMode.LOCAL:
            if had_fails:
                preamble = "[bold red]Trial finished with failures![/bold red]"
                logger.error(
                    f"Trial finished with failures! See results in {runner.workdir.resolve()}",
                    extra={"file_only": True},
                )
            else:
                preamble = "[bold green]Trial finished![/bold green]"
                logger.info(
                    f"Trial finished! See results in {runner.workdir.resolve()}",
                    extra={"file_only": True},
                )
            console.print(
                f"\n{preamble} Results located at [italic underline]{runner.workdir.resolve()}[/italic underline]"
            )
        case ExecutionMode.SLURM:
            if had_fails:
                finished_msg = "[bold red]Failed to orchestrate SLURM trial![/bold red]"
                logger.error(
                    "Failed to orchestrate SLURM trial!",
                    extra={"file_only": True},
                )
            else:
                finished_msg = (
                    "[bold green]SLURM trial orchestration finished![/bold green]"
                )
                logger.info(
                    "SLURM trial orchestration finished!",
                    extra={"file_only": True},
                )
            console.print(f"\n{finished_msg}")

    raise typer.Exit(code=1 if had_fails else 0)

init_project

Python
init_project(
    optimizer: Annotated[
        bool,
        Option(
            "--optimizer/--no-optimizer",
            "-op/-nop",
            help="Include an [bold cyan]objective[/bold cyan] function stub for use with [bold]mujoco-mojo run optimization[/bold].",
        ),
    ] = False,
) -> None

[bold yellow]Initialize a new empty mujoco-mojo project.[/bold yellow]

Writes a [bold cyan]simulation.py[/bold cyan] scaffold to the current directory containing stub implementations of [bold]generate[/bold], [bold]runtime[/bold], and [bold]UserData[/bold]. Pass [bold cyan]--optimizer[/bold cyan] to also include an [bold]objective[/bold] stub.

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@cli_app.command(name="init")
def init_project(
    optimizer: Annotated[
        bool,
        typer.Option(
            "--optimizer/--no-optimizer",
            "-op/-nop",
            help="Include an [bold cyan]objective[/bold cyan] function stub for use with [bold]mujoco-mojo run optimization[/bold].",
        ),
    ] = False,
) -> None:
    """
    [bold yellow]Initialize a new empty mujoco-mojo project.[/bold yellow]

    Writes a [bold cyan]simulation.py[/bold cyan] scaffold to the current directory containing stub implementations of [bold]generate[/bold], [bold]runtime[/bold], and [bold]UserData[/bold]. Pass [bold cyan]--optimizer[/bold cyan] to also include an [bold]objective[/bold] stub.
    """
    import os
    from importlib.resources import files

    tmpl = files("mujoco_mojo.templates")

    py_dest = Path("simulation.py")
    run_dest = Path("run.sh")
    reloaded_dest = Path("reloaded.sh")

    conflicts = [p for p in (py_dest, run_dest, reloaded_dest) if p.exists()]
    if conflicts:
        names = ", ".join(f"[bold cyan]{p}[/bold cyan]" for p in conflicts)
        console.print(
            f"[bold red]Error:[/bold red] {names} already exist. "
            "Remove them first or rename them before running init."
        )
        raise typer.Exit(code=1)

    py_template = "optimization.py" if optimizer else "monte_carlo.py"
    sh_template = "run_opt.sh" if optimizer else "run_mc.sh"

    py_dest.write_text(
        tmpl.joinpath(py_template).read_text(encoding="utf-8"), encoding="utf-8"
    )
    run_dest.write_text(
        tmpl.joinpath(sh_template).read_text(encoding="utf-8"), encoding="utf-8"
    )
    reloaded_dest.write_text(
        tmpl.joinpath("reloaded.sh").read_text(encoding="utf-8"), encoding="utf-8"
    )

    os.chmod(run_dest, 0o755)
    os.chmod(reloaded_dest, 0o755)

    w = max(len(str(p)) for p in (py_dest, run_dest, reloaded_dest))
    console.print(
        Panel(
            f"[bold green]Project initialized![/bold green]\n\n"
            f"  [bold cyan]{str(py_dest).ljust(w)}[/bold cyan]  - simulation stubs\n"
            f"  [bold cyan]{str(run_dest).ljust(w)}[/bold cyan]  - run the campaign\n"
            f"  [bold cyan]{str(reloaded_dest).ljust(w)}[/bold cyan]  - interactive viewer\n\n"
            f"[white]Get started:[/white]\n"
            f"    [bold yellow]bash run.sh[/bold yellow]",
            title="[cyan]Mojo Init[/cyan]",
            expand=False,
            border_style="cyan",
        )
    )

    settings_init(force=False, project=True)

    from rich.prompt import Confirm

    from mujoco_mojo.settings import GLOBAL_SETTINGS_FILE

    if not GLOBAL_SETTINGS_FILE.exists():
        console.print("\n[bold yellow]Global Settings[/bold yellow]")
        if Confirm.ask(
            "No user settings file found. Set one up now with defaults?", default=True
        ):
            settings_init(force=False)
        else:
            console.print(
                "You can set it up later with:\n"
                "    [bold cyan]mujoco-mojo settings init[/bold cyan]"
            )

    if not Path("typings", "mujoco").exists():
        console.print("\n[bold yellow]Type Hints Setup[/bold yellow]")
        console.print(
            "[bold white]MuJoCo does not ship Python type stubs.[/bold white] "
            "Generate them once for Pylance/Pyright autocomplete:\n\n"
            "[bold yellow]"
            "pip install pybind11-stubgen\n"
            "pybind11-stubgen mujoco -o typings/ --numpy-array-wrap-with-annotated"
            "[/bold yellow]\n\n"
            "Run from the project root. Then add to [bold cyan]pyproject.toml[/bold cyan]:\n\n"
            "[dim]"
            "\\[tool.pyright]\n"
            'stubPath = "typings"\n'
            'venvPath = "."\n'
            'venv    = ".venv"'
            "[/dim]\n\n"
            "[dim]Enum errors? Run: "
            "python typings/patch_mujoco_enums.py typings/mujoco/_enums.pyi[/dim]"
        )

settings_init

Python
settings_init(
    force: Annotated[
        bool,
        Option(
            "--force",
            "-f",
            help="Overwrite an existing settings file (will not overwrite existing settings).",
        ),
    ] = False,
    project: Annotated[
        bool,
        Option(
            "--project",
            "-p",
            help="Initialize the project-local settings file (./.mujoco-mojo/settings.toml) instead of the global one.",
        ),
    ] = False,
) -> None

[bold yellow]Initialize a settings file with defaults.[/bold yellow]

Writes [bold cyan]~/.mujoco-mojo/settings.toml[/bold cyan] (or, with [bold]--project[/bold], [bold cyan]./.mujoco-mojo/settings.toml[/bold cyan]) plus a [bold]settings.schema.json[/bold] and [bold].taplo.toml[/bold] colocated in the same directory, for TOML editor intellisense. Safe to re-run any time to refresh the schema, e.g. after upgrading mujoco-mojo. A project file is meant to hold only the specific keys that differ from the global settings (e.g. per-project SLURM extras or force-scaling), not a full copy - and gets its own self-contained schema, independent of the global one, so intellisense keeps working even when only the project directory is visible to your editor (e.g. opened over SSH/SSHFS without the home directory mounted).

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@settings_app.command(name="init")
def settings_init(
    force: Annotated[
        bool,
        typer.Option(
            "--force",
            "-f",
            help="Overwrite an existing settings file (will not overwrite existing settings).",
        ),
    ] = False,
    project: Annotated[
        bool,
        typer.Option(
            "--project",
            "-p",
            help="Initialize the project-local settings file (./.mujoco-mojo/settings.toml) instead of the global one.",
        ),
    ] = False,
) -> None:
    """
    [bold yellow]Initialize a settings file with defaults.[/bold yellow]

    Writes [bold cyan]~/.mujoco-mojo/settings.toml[/bold cyan] (or, with [bold]--project[/bold], [bold cyan]./.mujoco-mojo/settings.toml[/bold cyan]) plus a [bold]settings.schema.json[/bold] and [bold].taplo.toml[/bold] colocated in the same directory, for TOML editor intellisense. Safe to re-run any time to refresh the schema, e.g. after upgrading mujoco-mojo. A project file is meant to hold only the specific keys that differ from the global settings (e.g. per-project SLURM extras or force-scaling), not a full copy - and gets its own self-contained schema, independent of the global one, so intellisense keeps working even when only the project directory is visible to your editor (e.g. opened over SSH/SSHFS without the home directory mounted).
    """
    from mujoco_mojo.settings import (
        SETTINGS_DIR,
        MujocoMojoSettings,
        project_settings_file,
    )

    target_dir = project_settings_file().parent if project else SETTINGS_DIR
    target = target_dir / "settings.toml"
    already_existed = target.exists()

    if project:
        # model_construct() bypasses env/TOML loading entirely - save(project=True)
        # never reads the instance's field values, and re-running this must work
        # even if the existing project file (or the unrelated global one) is
        # currently malformed, since it never touches an existing file's contents
        MujocoMojoSettings.model_construct().save(target_dir, project=True)
        setting_msg = (
            f"[yellow]Project settings already exist:[/yellow] {target}"
            if already_existed
            else f"[green]Project settings written:[/green] {target}"
        )
        extra = "[white]Add only the keys you want to override for this project.\n\n"
        title = "Project Settings Initialized"
    elif already_existed and not force:
        MujocoMojoSettings.write_schema_files(target_dir)
        setting_msg = f"[yellow]Settings already exist:[/yellow] {target} [dim](Pass [bold]--force[/bold] to overwrite.)[/dim]"
        extra = ""
        title = "Settings Initialized"
    else:
        MujocoMojoSettings().save(target_dir)
        setting_msg = f"[green]Settings written:[/green]       {target}"
        extra = ""
        title = "Settings Initialized"

    schema_file = target_dir / "settings.schema.json"
    taplo_file = target_dir / ".taplo.toml"

    console.print(
        Panel(
            f"{setting_msg}\n"
            f"[green]Schema:[/green]                 {schema_file}\n"
            f"[green]Taplo config:[/green]           {taplo_file}\n\n"
            f"{extra}"
            "[white]TOML intellisense is now available for any taplo-powered editor "
            "(VS Code Even Better TOML, Neovim, etc.) with no additional configuration.",
            title=f"[cyan]{title}[/cyan]",
            expand=False,
            border_style="cyan",
        )
    )

settings_show

Python
settings_show(
    scope: Annotated[
        SettingsScope,
        Option(
            "--scope",
            "-s",
            help="Which settings to display.",
            case_sensitive=False,
        ),
    ] = EFFECTIVE,
) -> None

[bold yellow]Display settings.[/bold yellow]

By default, resolves values from all sources in priority order (environment variables, project settings file, global settings file, then defaults) and shows the resulting effective settings, with the API key masked. Pass [bold]--scope project[/bold] or [bold]--scope global[/bold] to instead show the raw, unmerged contents of just that one file (also masked).

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@settings_app.command(name="show")
def settings_show(
    scope: Annotated[
        SettingsScope,
        typer.Option(
            "--scope",
            "-s",
            help="Which settings to display.",
            case_sensitive=False,
        ),
    ] = SettingsScope.EFFECTIVE,
) -> None:
    """
    [bold yellow]Display settings.[/bold yellow]

    By default, resolves values from all sources in priority order (environment variables, project settings file, global settings file, then defaults) and shows the resulting effective settings, with the API key masked. Pass [bold]--scope project[/bold] or [bold]--scope global[/bold] to instead show the raw, unmerged contents of just that one file (also masked).
    """
    import tomlkit
    from rich.syntax import Syntax

    from mujoco_mojo.settings import GLOBAL_SETTINGS_FILE, project_settings_file

    if scope is not SettingsScope.EFFECTIVE:
        target = (
            project_settings_file()
            if scope is SettingsScope.PROJECT
            else GLOBAL_SETTINGS_FILE
        )
        label = scope.value.capitalize()
        if not target.exists():
            init_cmd = (
                "mujoco-mojo settings init --project"
                if scope is SettingsScope.PROJECT
                else "mujoco-mojo settings init"
            )
            console.print(
                f"[yellow]No {scope.value} settings file found.[/yellow] "
                f"Run [bold cyan]{init_cmd}[/bold cyan] to create one."
            )
            return

        doc = tomlkit.parse(target.read_text(encoding="utf-8"))
        match scope:
            case SettingsScope.PROJECT:
                target_str = str(target)
            case SettingsScope.GLOBAL:
                target_str = GLOBAL_SETTINGS_FILE.relative_to(Path.home()).as_posix()
        raw_str = _pad_for_subtitle(tomlkit.dumps(doc).rstrip("\n"), target_str)

        console.print(
            Panel(
                Syntax(raw_str, "toml", theme="ansi_dark"),
                title=f"[cyan]{label} Settings File[/cyan]",
                subtitle=f"[dim]{target_str}[/dim]",
                expand=False,
                border_style="cyan",
            )
        )
        return

    from mujoco_mojo.settings import MujocoMojoSettings

    settings = MujocoMojoSettings()
    d = settings.model_dump(mode="json", exclude_none=True)

    toml_str = tomlkit.dumps(d).rstrip("\n")
    if GLOBAL_SETTINGS_FILE.exists():
        try:
            source = "~/" + str(
                GLOBAL_SETTINGS_FILE.relative_to(Path.home()).as_posix()
            )
        except ValueError:
            breakpoint()
            source = str(GLOBAL_SETTINGS_FILE)
    else:
        source = "defaults only"

    project_file = project_settings_file()
    if project_file.exists():
        source += f" + {project_file.relative_to(Path.cwd()).as_posix()}"

    toml_str = _pad_for_subtitle(toml_str, source)

    console.print(
        Panel(
            Syntax(toml_str, "toml", theme="ansi_dark"),
            title="[cyan]MuJoCo Mojo Settings[/cyan]",
            subtitle=f"[dim]{source}[/dim]",
            expand=False,
            border_style="cyan",
        )
    )

settings_set_cmd

Python
settings_set_cmd(
    key: Annotated[
        str,
        Argument(
            help="Dotted key path (e.g. [bold cyan]dojo.sensai.enabled[/bold cyan])",
            show_default=False,
        ),
    ],
    value: Annotated[
        str,
        Argument(
            help="Value to assign. Integers, floats, and [bold]True[/bold]/[bold]False[/bold] are auto-parsed.",
            show_default=False,
        ),
    ],
    project: Annotated[
        bool,
        Option(
            "--project",
            "-p",
            help="Write to the project-local settings file (./.mujoco-mojo/settings.toml) instead of the global one.",
        ),
    ] = False,
) -> None

[bold yellow]Update a setting.[/bold yellow]

Example: [bold cyan]mujoco-mojo settings set dojo.sensai.enabled true[/bold cyan]

Add [bold]--project[/bold] to write only the changed key into this project's local override file, leaving everything else there untouched.

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@settings_app.command(name="set")
def settings_set_cmd(
    key: Annotated[
        str,
        typer.Argument(
            help="Dotted key path (e.g. [bold cyan]dojo.sensai.enabled[/bold cyan])",
            show_default=False,
        ),
    ],
    value: Annotated[
        str,
        typer.Argument(
            help="Value to assign. Integers, floats, and [bold]True[/bold]/[bold]False[/bold] are auto-parsed.",
            show_default=False,
        ),
    ],
    project: Annotated[
        bool,
        typer.Option(
            "--project",
            "-p",
            help="Write to the project-local settings file (./.mujoco-mojo/settings.toml) instead of the global one.",
        ),
    ] = False,
) -> None:
    """
    [bold yellow]Update a setting.[/bold yellow]

    Example: [bold cyan]mujoco-mojo settings set dojo.sensai.enabled true[/bold cyan]

    Add [bold]--project[/bold] to write only the changed key into this project's local override file, leaving everything else there untouched.
    """
    from pydantic import ValidationError

    from mujoco_mojo.settings import GLOBAL_SETTINGS_FILE, MujocoMojoSettings

    parsed = _smart_parse(value)

    if project:
        try:
            MujocoMojoSettings.set_project_value(key, parsed)
        except KeyError:
            console.print(
                f"[bold red]Error:[/bold red] Unknown settings path: [bold cyan]{key}[/bold cyan]"
            )
            raise typer.Exit(code=1)
        except ValidationError as e:
            console.print(f"[bold red]Validation error:[/bold red] {e}")
            raise typer.Exit(code=1)
        console.print(
            f"[green]Updated project setting[/green] [bold cyan]{key}[/bold cyan] = {parsed!r}"
        )
        return

    if not GLOBAL_SETTINGS_FILE.exists():
        console.print(
            "[bold red]Error:[/bold red] No settings file found. "
            "Run [bold cyan]mujoco-mojo settings init[/bold cyan] first."
        )
        raise typer.Exit(code=1)

    settings = MujocoMojoSettings()
    data = settings.model_dump()

    parts = key.split(".")
    target: dict[str, Any] = data
    for part in parts[:-1]:
        if part not in target or not isinstance(target[part], dict):
            console.print(
                f"[bold red]Error:[/bold red] Unknown settings path: [bold cyan]{key}[/bold cyan]"
            )
            raise typer.Exit(code=1)
        target = target[part]

    leaf = parts[-1]
    if leaf not in target:
        console.print(
            f"[bold red]Error:[/bold red] Unknown settings key: [bold cyan]{key}[/bold cyan]"
        )
        raise typer.Exit(code=1)

    target[leaf] = parsed

    try:
        updated = MujocoMojoSettings.model_validate(data)
    except ValidationError as e:
        console.print(f"[bold red]Validation error:[/bold red] {e}")
        raise typer.Exit(code=1)

    updated.save()
    console.print(f"[green]Updated[/green] [bold cyan]{key}[/bold cyan] = {parsed!r}")

settings_reset

Python
settings_reset(
    project: Annotated[
        bool,
        Option(
            "--project",
            "-p",
            help="Delete the project-local settings file (./.mujoco-mojo/settings.toml) instead of resetting the global one.",
        ),
    ] = False,
) -> None

[bold yellow]Reset settings back to their defaults.[/bold yellow]

Without [bold]--project[/bold]: overwrites every value in [bold cyan]~/.mujoco-mojo/settings.toml[/bold cyan] with its default (comments you've added by hand are kept). With [bold]--project[/bold]: deletes the project-local override file entirely, since it's meant to hold only a small, deliberate diff, not a full copy of every setting.

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@settings_app.command(name="reset")
def settings_reset(
    project: Annotated[
        bool,
        typer.Option(
            "--project",
            "-p",
            help="Delete the project-local settings file (./.mujoco-mojo/settings.toml) instead of resetting the global one.",
        ),
    ] = False,
) -> None:
    """
    [bold yellow]Reset settings back to their defaults.[/bold yellow]

    Without [bold]--project[/bold]: overwrites every value in [bold cyan]~/.mujoco-mojo/settings.toml[/bold cyan] with its default (comments you've added by hand are kept). With [bold]--project[/bold]: deletes the project-local override file entirely, since it's meant to hold only a small, deliberate diff, not a full copy of every setting.
    """
    from rich.prompt import Confirm

    from mujoco_mojo.settings import MujocoMojoSettings, project_settings_file

    if project:
        target = project_settings_file()
        if not target.exists():
            console.print(
                "[yellow]No project settings file found.[/yellow] Nothing to reset."
            )
            return
        if not Confirm.ask(
            f"Are you sure you want to delete your project settings file ({target})?",
            default=False,
        ):
            console.print("[yellow]Aborted.[/yellow] No changes made.")
            raise typer.Exit()
        target.unlink()
        console.print("[green]Project settings file deleted.[/green]")
        return

    if not Confirm.ask(
        "Are you sure you want to reset all settings to their defaults?",
        default=False,
    ):
        console.print("[yellow]Aborted.[/yellow] No changes made.")
        raise typer.Exit()

    MujocoMojoSettings.defaults().save()
    console.print("[green]Settings reset to defaults.[/green]")

run_reloaded

Python
run_reloaded(
    generator: ReloadedGeneratorType = None,
    runtime: RuntimeType = DEFAULT_RUNTIME,
    workdir: WorkdirType = DEFAULT_WORKDIR,
    ui: UIType = ui,
    overrides_path: OverridesType = None,
    trial_num: TrialNumType = 0,
    seed: SeedType = DEFAULT_SEED,
    config_path: ConfigPathFileType = None,
    model_config_name: ModelConfigNameType = model_config_name,
    xml_name: XMLNameType = xml_name,
    watch: WatchType = watch,
    record: RecordType = record,
    gen_args: GenArgsType = [],
    gen_kwargs: GenKwargsType = [],
    run_args: RunArgsType = [],
    run_kwargs: RunKwargsType = [],
    host: HostType = default_host,
    port: PortType = default_port,
    verbose: VerboseType = 0,
    quiet: QuietType = 0,
) -> None

[bold yellow]Run a development session with the native OpenGL viewer or a web browser based GUI.[/bold yellow]

Manual trigger to regenerate and reload the MJCF model for rapid prototyping.

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@cli_app.command(name="reloaded")
def run_reloaded(
    generator: ReloadedGeneratorType = None,
    runtime: RuntimeType = DEFAULT_RUNTIME,
    workdir: WorkdirType = DEFAULT_WORKDIR,
    ui: UIType = _SETTINGS.reloaded.ui,
    overrides_path: OverridesType = None,
    trial_num: TrialNumType = 0,
    seed: SeedType = DEFAULT_SEED,
    config_path: ConfigPathFileType = None,
    model_config_name: ModelConfigNameType = _SETTINGS.general.model_config_name,
    xml_name: XMLNameType = _SETTINGS.general.xml_name,
    watch: WatchType = _SETTINGS.reloaded.watch,
    record: RecordType = _SETTINGS.reloaded.record,
    gen_args: GenArgsType = [],
    gen_kwargs: GenKwargsType = [],
    run_args: RunArgsType = [],
    run_kwargs: RunKwargsType = [],
    host: HostType = _SETTINGS.general.default_host,
    port: PortType = _SETTINGS.general.default_port,
    verbose: VerboseType = 0,
    quiet: QuietType = 0,
) -> None:
    """
    [bold yellow]Run a development session with the native OpenGL viewer or a web browser based GUI.[/bold yellow]

    Manual trigger to regenerate and reload the MJCF model for rapid prototyping.
    """
    from .reloaded import MojoReloaded

    port = find_free_port(host, port)

    _logger = _setup_cli_logging(
        verbose=_SETTINGS.general.verbose + verbose,
        quiet=_SETTINGS.general.quiet + quiet,
    )

    # initialize and resolve
    overrides_path = None if not overrides_path else overrides_path.resolve()
    processed_gen_args = [_smart_parse(a) for a in gen_args]
    processed_gen_kwargs = _process_kwargs(gen_kwargs)
    processed_run_args = [_smart_parse(a) for a in run_args]
    processed_run_kwargs = _process_kwargs(run_kwargs)

    MojoReloaded(
        generator=generator,
        runtime=runtime,
        workdir=workdir,
        ui=ui,
        overrides_path=overrides_path,
        config_path=config_path,
        trial_num=trial_num,
        seed=seed,
        model_config_name=model_config_name,
        xml_name=xml_name,
        watch=watch,
        record=record,
        gen_args=processed_gen_args,
        gen_kwargs=processed_gen_kwargs,
        run_args=processed_run_args,
        run_kwargs=processed_run_kwargs,
        host=host,
        port=port,
    ).run()

run_dojo

Python
run_dojo(
    ctx: Context,
    workdir: DojoWorkdirType,
    host: HostType = default_host,
    port: PortType = default_port,
    n_proc: NProcType = n_proc,
    password: DojoPassword = None,
    verbose: VerboseType = 0,
    quiet: QuietType = 0,
) -> None

[bold yellow]Launch the Mojo Dojo to monitor the progress of a running job.[/bold yellow]

This command reads status files in the workdir to give live updates.

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@cli_app.command(name="dojo")
def run_dojo(
    ctx: typer.Context,
    workdir: DojoWorkdirType,
    host: HostType = _SETTINGS.general.default_host,
    port: PortType = _SETTINGS.general.default_port,
    n_proc: NProcType = _SETTINGS.general.n_proc,
    # deliberately kept as a None sentinel, unlike every other option above -
    # baking the resolved settings value in as a literal default here would
    # print the real password in plaintext every time --help runs.
    password: DojoPassword = None,
    verbose: VerboseType = 0,
    quiet: QuietType = 0,
) -> None:
    """
    [bold yellow]Launch the Mojo Dojo to monitor the progress of a running job.[/bold yellow]

    This command reads status files in the workdir to give live updates.
    """
    import warnings

    import uvicorn

    if password is None and _SETTINGS.dojo.password is not None:
        password = _SETTINGS.dojo.password.get_secret_value()
    port = find_free_port(host, port)

    _logger = _setup_cli_logging(
        verbose=_SETTINGS.general.verbose + verbose,
        quiet=_SETTINGS.general.quiet + quiet,
    )

    # fastmcp (a transitive dependency of the sensai agent) pulls in key_value.aio,
    # which calls beartype_this_package() on itself. beartype then trips over
    # numpydantic's NDArray metaclass, which breaks isinstance() checks.
    # this is purely a third-party incompatibility; suppress the noise.
    warnings.filterwarnings("ignore", module=r"key_value\.")

    import mujoco_mojo.utils.layers.dojo.shared as shared
    from mujoco_mojo.utils.layers.dojo.main import dojo_app
    from mujoco_mojo.utils.statusing import JOB_STATUS_FNAME, JobStatus, JobType

    status_file = (workdir / JOB_STATUS_FNAME).resolve()
    if not status_file.exists():
        console.print(
            f"[bold red]Error:[/bold red] No status file found at {status_file}"
        )
        raise typer.Exit(code=1)

    # read job status file for monitoring and inject
    job = JobStatus.model_validate_json(status_file.read_text())
    job.refresh_from_disk(n_proc=n_proc, persist=False)

    shared.CURRENT_JOB = job
    shared.AUTH_PASSWORD = password
    shared.set_globals(workdir=workdir, owner=job.started_by, job_type=job.job_type)

    if job.job_type == JobType.OPTIMIZE:
        from mujoco_mojo.utils.layers.dojo.routers.morph import mount_optuna_engine

        db_path = f"sqlite:///{workdir / 'study.db'}"
        try:
            mount_optuna_engine(dojo_app, db_path)
        except ModuleNotFoundError as e:
            console.print(f"[bold red]Error:[/bold red] {escape(str(e))}")
            raise typer.Exit(code=1)

    # detect ip
    local_ip = get_local_ip()
    if password:
        connection_info = "[yellow]Auth enabled. Use [u]any[/u] username and your provided password.[/yellow]\n\n"
    else:
        connection_info = ""

    connection_info += f"Local: [bold cyan u]http://127.0.0.1:{port}[/bold cyan u]"

    if host == "0.0.0.0":
        connection_info += (
            f"\nMobile: [bold cyan u]http://{local_ip}:{port}[/bold cyan u]"
        )
    else:
        connection_info += "\n\n[dim]Tip: To view on other devices (and to make pages shareable), run with[/dim] [yellow]--host 0.0.0.0[/yellow]"

    console.print(
        Panel(
            f"""[bold green]MuJoCo Mojo Dojo is Live![/bold green]\n\n{connection_info}\n\n[yellow]Press CTRL+C to stop[/yellow]""",
            border_style="cyan",
            expand=False,
            title="Mojo Dojo",
            subtitle=f"Workers: {n_proc}",
        )
    )

    # uvicorn.run(dojo_app, host=host, port=port, log_level="info")
    uvicorn.run(dojo_app, host=host, port=port, log_level="critical")

run_optimizer

Python
run_optimizer(
    ctx: Context,
    generator: GeneratorType,
    objective: ObjectiveType,
    runtime: RuntimeType = DEFAULT_RUNTIME,
    workdir: WorkdirType = DEFAULT_WORKDIR,
    n_trial: NTrialType = DEFAULT_OP_N_TRIAL,
    n_proc: NProcType = n_proc,
    timeout: TimeoutType = timeout,
    study_name: StudyNameType = study_name,
    direction: DirectionType = direction,
    sampler: SamplerType = sampler,
    storage: StorageType = storage,
    resume: ResumeType = resume,
    seed: SeedType = DEFAULT_SEED,
    evals_per_trial: EvalsPerTrialType = evals_per_trial,
    refine_search_factor: RefineSearchFactorType = refine_search_factor,
    prune_failed_trials: PruneFailedTrialsType = prune_failed_trials,
    clean_workdir: CleanWorkdirType = clean_workdir,
    model_config_name: ModelConfigNameType = model_config_name,
    xml_name: XMLNameType = xml_name,
    overrides: OverridesType = None,
    gen_args: GenArgsType = [],
    gen_kwargs: GenKwargsType = [],
    run_args: RunArgsType = [],
    run_kwargs: RunKwargsType = [],
    verbose: VerboseType = 0,
    quiet: QuietType = 0,
) -> None

[bold yellow]Execute an automated Design Optimization study.[/bold yellow]

This command uses Optuna to intelligently navigate the search space defined by [bold cyan]model.design_float[/bold cyan] and [bold cyan]model.design_categorical[/bold cyan] calls within your generator.

Source code in src/mujoco_mojo/utils/layers/cli.py
Python
@run_app.command(name="optimization")
def run_optimizer(
    ctx: typer.Context,
    generator: GeneratorType,
    objective: ObjectiveType,
    runtime: RuntimeType = DEFAULT_RUNTIME,
    workdir: WorkdirType = DEFAULT_WORKDIR,
    n_trial: NTrialType = DEFAULT_OP_N_TRIAL,
    n_proc: NProcType = _SETTINGS.general.n_proc,
    timeout: TimeoutType = _SETTINGS.run.optimize.timeout,
    study_name: StudyNameType = _SETTINGS.run.optimize.study_name,
    direction: DirectionType = _SETTINGS.run.optimize.direction,
    sampler: SamplerType = _SETTINGS.run.optimize.sampler,
    storage: StorageType = _SETTINGS.run.optimize.storage,
    resume: ResumeType = _SETTINGS.run.optimize.resume,
    seed: SeedType = DEFAULT_SEED,
    evals_per_trial: EvalsPerTrialType = _SETTINGS.run.optimize.evals_per_trial,
    refine_search_factor: RefineSearchFactorType = _SETTINGS.run.optimize.refine_search_factor,
    prune_failed_trials: PruneFailedTrialsType = _SETTINGS.run.optimize.prune_failed_trials,
    clean_workdir: CleanWorkdirType = _SETTINGS.run.optimize.clean_workdir,
    model_config_name: ModelConfigNameType = _SETTINGS.general.model_config_name,
    xml_name: XMLNameType = _SETTINGS.general.xml_name,
    overrides: OverridesType = None,
    gen_args: GenArgsType = [],
    gen_kwargs: GenKwargsType = [],
    run_args: RunArgsType = [],
    run_kwargs: RunKwargsType = [],
    verbose: VerboseType = 0,
    quiet: QuietType = 0,
) -> None:
    """
    [bold yellow]Execute an automated Design Optimization study.[/bold yellow]

    This command uses Optuna to intelligently navigate the search space defined by [bold cyan]model.design_float[/bold cyan] and [bold cyan]model.design_categorical[/bold cyan] calls within your generator.
    """
    try:
        import optuna
    except ModuleNotFoundError:
        console.print(
            "[bold red]Error:[/bold red] The `optuna` package is required for optimization. "
            r"Install with [bold]uv add mujoco-mojo\[optimize][/bold] or "
            r"[bold]pip install mujoco-mojo\[optimize][/bold]"
        )
        raise typer.Exit(code=1)
    from numpydantic import NDArray

    from mujoco_mojo.stochas import NamedValueDict
    from mujoco_mojo.utils.runner import OptimizerConfig

    optuna.logging.set_verbosity(optuna.logging.WARNING)

    logger = _setup_cli_logging(
        verbose=_SETTINGS.general.verbose + verbose,
        quiet=_SETTINGS.general.quiet + quiet,
    )
    print_logo()

    workdir = workdir.resolve()

    dojo_cmd = f'mujoco-mojo dojo "{workdir}"'
    console.print(
        Panel(
            f"[bold green]Optimization Engine Engaged![/]\n\n"
            f"[white]Study:[/]      [cyan]{study_name}[/]\n"
            f"[white]Direction:[/]  [magenta]{direction}[/]\n"
            f"[white]Sampler:[/]    [yellow]{sampler}[/]\n\n"
            f"[white]To monitor live progress, run:[/]\n"
            f"    [bold yellow]{dojo_cmd}[/]",
            title="[cyan]Optimizer Initialized[/]",
            expand=False,
            border_style="cyan",
        )
    )

    global_overrides = None
    if overrides:
        overrides = overrides.resolve()
        logger.info(f"Loading global NamedValue overrides from `{overrides}`")
        global_overrides = NamedValueDict[NDArray].model_validate_json(
            overrides.read_text()
        )

    runner = _prepare_runner(
        generator=generator,
        runtime=runtime,
        workdir=workdir,
        objective=objective,
        model_config_name=model_config_name,
        seed=seed,
        xml_name=xml_name,
        gen_args=gen_args,
        gen_kwargs=gen_kwargs,
        run_args=run_args,
        run_kwargs=run_kwargs,
    )

    runner.config = OptimizerConfig(
        n_trial=n_trial,
        n_proc=n_proc,
        timeout=timeout,
        study_name=study_name,
        direction=direction,
        sampler=sampler,
        storage=f"sqlite:///{workdir / 'study.db'}" if storage else None,
        resume=resume,
        evals_per_trial=evals_per_trial,
        refine_search_factor=refine_search_factor,
        prune_failed_trials=prune_failed_trials,
    )

    try:
        had_fails = runner.run(
            global_overrides=global_overrides
            if global_overrides
            else NamedValueDict[NDArray](),
            clean_workdir=clean_workdir,
            execution_mode=ExecutionMode.LOCAL,
        )
    except ModuleNotFoundError as e:
        console.print(f"[bold red]Error:[/bold red] {escape(str(e))}")
        raise typer.Exit(code=1)

    if had_fails:
        console.print(
            f"\n[bold red]Optimization finished with errors.[/] See logs in {workdir}"
        )
    else:
        console.print(
            f"\n[bold green]Optimization study complete![/] Best parameters saved to [italic]{workdir}/best_params.json[/italic]"
        )

    raise typer.Exit(code=1 if had_fails else 0)