# Nmp CLI

> Use when developing the NeMo Platform CLI - adding commands, modifying the generator, templates, or configuration. Covers both manual commands (use-cases, config) and auto-generated API commands. Also helps troubleshoot CLI generation failures (e.g., Typer errors after updates).

- Skill: `nvidia-nemo/nmp-cli` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nvidia-nemo/nmp-cli`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nvidia-nemo/nmp-cli/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: NVIDIA NeMo (https://skillmd.com/u/nvidia-nemo)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nvidia-nemo/nmp-cli

---


# NeMo Platform CLI Development

The CLI provides a command-line interface for NeMo Platform, implemented using Typer in `packages/nemo_platform_ext/src/nemo_platform_ext/cli/`.

## Command Categories

| Category | Location | Description | Editable? |
|----------|----------|-------------|-----------|
| **API** | `commands/api/` | Auto-generated from SDK | ❌ Never edit |
| **Configuration** | `commands/config.py`, `commands/configure.py` | Config management | ✅ Yes |
| **Use-case** | `commands/use_cases/` | High-level commands (e.g., `chat`) | ✅ Yes |
| **Quickstart** | `commands/quickstart/` | Local deployment | ✅ Yes |

## Directory Structure

```
packages/nemo_platform_ext/src/nemo_platform_ext/cli/
├── app.py                    # Entry point, command registration, global options
├── core/                     # Shared utilities
└── commands/
    ├── config.py             # kubectl-style config management
    ├── configure.py          # Interactive configuration
    ├── quickstart/           # Local deployment commands
    ├── use_cases/            # High-level commands (chat, wait)
    └── api/                  # ⚠️ AUTO-GENERATED - DO NOT EDIT
```

## Building the CLI

### Development Shortcut

During local CLI development, you can run `_nemo` to execute the CLI directly from `packages/nemo_platform_ext`.
This lets you test changes immediately without running vendoring first.

```bash
uv run _nemo --help
```

Use vendoring (`make vendor-nemo-platform-ext`) when you need to validate behavior in `sdk/python/nemo-platform`.

Build everything (recommended):
```bash
make update-cli
```

Individual steps:
```bash
# Step 1: Generate CLI commands from SDK
make generate-cli-commands

# Step 2: Vendor into SDK package
make vendor-nemo-platform-ext

# Step 3: Generate documentation
make generate-cli-reference-docs
```

## Adding a Manual Command

Manual commands go in `commands/use_cases/` or directly in `commands/`.

### Pattern

```python
from __future__ import annotations

from typing import Annotated

import typer

from nemo_platform_ext.cli.core.context import CLIContext
from nemo_platform_ext.cli.core.errors import handle_errors


@handle_errors
def my_command(
    ctx: typer.Context,
    name: Annotated[str, typer.Argument(help="The name argument")],
    option: Annotated[str | None,
        typer.Option(
            "--option",
            "-o",
            help="An optional flag",
            rich_help_panel="Options",
        ),
    ] = None,
) -> None:
    """
    Short description of what the command does.

    Examples:
      nemo my-command foo
      nemo my-command bar --option value
    """
    state: CLIContext = ctx.obj
    client = state.get_client()

    # Use client to make API calls
    result = client.some_resource.some_method(name=name)

    # Output result (use state.output() for formatted output)
    state.output(result)
```

### Register in app.py

```python
from nemo_platform_ext.cli.commands.my_module import my_command

# Single command
app.command(name="my-command", rich_help_panel="Use cases")(my_command)

# Command group (sub-app)
app.add_typer(my_sub_app, name="my-group", rich_help_panel="Category")
```

## CLI Generator

The generator creates commands in `commands/api/` from SDK introspection.

### Key Files

```
tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/cli_generator/
├── cli_config.yaml           # Generation configuration
├── generator.py              # Main generation logic
├── sdk_introspector.py       # SDK introspection
├── operation_classifier.py   # Classifies methods (list, get, create, etc.)
├── templates/                # Jinja2 templates
├── context_collectors/       # Collect template context per operation type
└── overrides/                # Custom command implementations
```

### Configuration (cli_config.yaml)

Configure table columns for `list` commands:
```yaml
- resource: [customization, jobs]
  methods:
    list:
      columns:
      - name
      - description
      - status
      - created_at
```

Skip a method:
```yaml
- resource: [filesets]
  methods:
    upload_file:
      skip: true
```

Use custom implementation:
```yaml
- resource: [secrets]
  methods:
    create:
      override: secrets/create.py
```

### Command Overrides

For methods requiring custom handling (file uploads, streaming), create overrides:

1. Add config in `cli_config.yaml`:
   ```yaml
   - resource: [files]
     additional_methods:
       upload:
         override: files/upload.py
   ```

2. Create override file in `overrides/files/upload.py`:
   ```python
   from typing import Annotated, Any, cast

   import typer

   from nemo_platform_ext.cli.core.context import CLIContext
   from nemo_platform_ext.cli.core.errors import handle_errors

   app = cast(Any, None)  # override-skip: provided by generated file


   @app.command("upload")
   @handle_errors
   def upload_files(
       ctx: typer.Context,
       path: Annotated[str, typer.Argument(help="Local path to upload")],
   ) -> None:
       """Upload files to a fileset."""
       state: CLIContext = ctx.obj
       client = state.get_client()
       # Custom implementation...
   ```

The `# override-skip` comment strips that line from output (useful for linter satisfaction).

### Templates

Templates use Jinja2. Key variables available:
- `command_name` - CLI command name (e.g., "list")
- `method_name` - SDK method name (e.g., "list")
- `resource_path` - Resource path (e.g., ["customization", "jobs"])
- `params` - List of parameter info
- `docstring` - Method docstring

## Core Utilities

### CLIContext

Access via `ctx.obj`:
```python
state: CLIContext = ctx.obj
client = state.get_client()  # Get SDK client
state.output(data)           # Format and print output
state.verbosity              # 0 or 1
```

### Error Handling

Always use `@handle_errors` decorator:
```python
from nemo_platform_ext.cli.core.errors import handle_errors

@handle_errors
def my_command(ctx: typer.Context) -> None:
    ...
```

### Building API Request Bodies

```python
from nemo_platform_ext.cli.core.api import build_kwargs

body = build_kwargs(
    model="my-model",
    messages=[{"role": "user", "content": "Hello"}],
    stream=True,
    temperature=0.7,
)
```

## Testing

Run CLI tests:
```bash
uv run pytest packages/nemo_platform_ext/tests/cli/ -v
```

Run generator tests:
```bash
uv run --package nemo-platform-sdk-tools pytest tools/nemo-platform-sdk-tools/tests/sdk/cli_generator/ -v
```

## Common Tasks

### Add table columns for a new resource

Edit `cli_config.yaml`:
```yaml
- resource: [my_service, my_resource]
  methods:
    list:
      columns:
      - name
      - status
      - created_at
```

Then rebuild: `make update-cli`

### Add a new use-case command

1. Create file in `commands/use_cases/my_command.py`
2. Implement using pattern above
3. Register in `app.py`
4. Vendor: `make vendor-nemo-platform-ext`

### Modify how a command is generated

1. Check which template is used (see `operation_classifier.py`)
2. Edit template in `templates/`
3. Rebuild: `make update-cli`

### Override an auto-generated command

1. Add override config in `cli_config.yaml`
2. Create override file in `overrides/`
3. Rebuild: `make update-cli`

## Troubleshooting

### Typer errors after CLI update

If tests fail with Typer errors after running `make update-cli`, the issue is usually one of:

**1. Unsupported type in Typer**

Typer doesn't support all Python types (e.g., complex unions, nested TypedDicts). Fix by updating the generator code to handle the type:

- Check `type_formatter.py` for type formatting
- Check `typed_dict_utils.py` for TypedDict handling
- Add type conversion or simplification in the relevant context collector

**2. Method miscategorized**

The operation classifier may assign the wrong template (e.g., "default" instead of "create"). Fix via `cli_config.yaml`:

```yaml
- resource: [my_service]
  methods:
    my_method:
      operation_type: create  # Override the auto-detected type
```

Available operation types: `list`, `get`, `create`, `update`, `delete`, `download`, `default`

**3. Skip the method temporarily**

If the fix is non-trivial, skip the method to unblock your work:

```yaml
- resource: [my_service]
  methods:
    problematic_method:
      skip: true  # TODO: Fix...
```

Create a GitLab issue to track the fix, then rebuild: `make update-cli`

