nhx-cli
Skill138 starsChanged 21 days agoApache-2.0View on GitHub
Use when developing the NeMo Helix CLI - adding or changing command groups, wiring a service's typed client into commands, output formatting, pagination, error mapping, or the `--output-format code` snippet generator. Covers core groups in nemo_helix_ext and plugin-hosted `nemo.cli` groups.
In this repository18 other files it gives its agents.
- AGENTS.mdAGENTS.md
- docs/AGENTS.mdAGENTS.md
- web/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- web/CLAUDE.mdCLAUDE.md
- .cursor/rules/dot-files.mdcCursor rule
- .cursor/rules/nemo-helix.mdcCursor rule
- .cursor/rules/uv.mdcCursor rule
- .cursor/rules/writing-docs.mdcCursor rule
- add-studio-feature-flagSkill
- contributor-create-prSkill
- create-release-notesSkill
- data-designer-skill-refreshSkill
- data-designer-version-bumpSkill
- release-test-scopeSkill
- studio-devSkill
- nemo-data-designer-pluginSkill
- nemo-evals-pluginSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 27
Every command talks to the platform through the typed clients in nemo_helix_plugin (nemo_helix_plugin.<area>.client, or a plugin's own …
- Very specific: line 155
Options: use the shared aliases (WorkspaceOption, ListOutputFormatOption, ...).
- Very specific: line 187
If the typed client lacks a route, add it to nemo_helix_plugin/<area>/{endpoints,types,client}.py (or the plugin's own types/endpoints.py + …
- Very specific: line 221
Command needs a helper the typed client lacks: check packages/filesets/src/filesets/transfer.py (fileset upload/download/list/delete) …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
NeMo Helix CLI Development
The nemo CLI host (entry point, global options, config, auth, lazy group loading) lives in
packages/nemo_helix_ext/src/nemo_helix_ext/cli/. The helpers commands are built from live in
packages/nemo_helix_plugin so core and plugin commands share them:
Module (nemo_helix_plugin.) | Provides |
cli | NemoCLI (plugin base class), create_typer_app, HELP_OPTION_NAMES |
cli_state | CLIState protocol, cli_state(ctx), resolve_output_format, resolve_cli_workspace |
cli_options | WorkspaceOption, ListOutputFormatOption, EntityOutputFormatOption, NoTruncateOption, OutputColumnsOption, StreamOutputOption, AllPagesOption, format Literals |
cli_output | format_output (unwraps NemoResponse), Column, check_output_columns_with_format, is_tty |
cli_pagination | collect_offset_pages, collect_cursor_pages, warn_if_more_pages, PaginationType |
cli_warnings | collect_warnings, add_warning |
cli_error_handling | handle_errors: typed-client errors -> messages and exit codes |
cli_input | build_request_body, read_data_input_with_flags, read_payload, validate_required_fields |
cli_kwargs | build_kwargs, merge_filter_dict |
cli_codegen | handle_code_generation for --output-format code |
Every command talks to the platform through the typed clients in nemo_helix_plugin
(nemo_helix_plugin.<area>.client, or a plugin's own client.py).
Plugin and service code must not import nemo_helix_ext — plugins only depend on
nemo_helix_plugin.
Command Categories
| Category | Location | Description |
| Core resource groups | commands/<group>.py (files, inference/, jobs, models, secrets, workspaces, hidden adapters, iam, projects) | Hand-written on typed clients, registered in commands/manifest_registry.py |
| Plugin-hosted groups | owning package, nemo.cli entry point (guardrail → plugins/nemo-guardrails, intake/experiments → services/intake, insights, agents, garak, ...) | Appear only when the package is installed |
| Generated job/function verbs | nemo_helix_plugin/commands.py | submit/explain/run for every NemoJob/NemoFunction a plugin registers |
| Setup / use cases | commands/setup.py, commands/use_cases/, commands/auth.py, commands/config.py | Wizards and workflows (chat, wait, ...) |
| Services | commands/services/, commands/quickstart/ | Run the platform locally (imports server packages by design) |
Directory Structure
packages/nemo_helix_ext/src/nemo_helix_ext/cli/
├── app.py # Entry point, global options (--base-url, --context, --output-format), lazy registration
├── manifest.py # TopLevelEntry, panel order
├── core/
│ ├── context.py # CLIContext: the concrete CLIState (config, auth, typed_client(XClient))
│ ├── help_formatter.py # NhxGroup/NhxCommand: NeMo help rendering, applied to every mounted command
│ ├── lazy_load.py # Loads core and plugin groups on demand
│ └── waiters.py # --wait / --watch helpers (deployments, jobs, gateway readiness)
└── commands/
├── manifest_registry.py # TOP_LEVEL_ENTRIES: every built-in group/command
├── secrets.py # REFERENCE port for core groups
└── ...
plugins/example-plugin/src/nemo_example_plugin/cli.py # REFERENCE for plugin groupsRunning the CLI During Development
uv run _nemo --help # runs from packages/nemo_helix_ext
make update-cli # regenerate the CLI reference docsAdding or Changing a Command Group
Templates: commands/secrets.py + tests/cli/commands/test_secrets.py for core groups,
plugins/example-plugin/src/nemo_example_plugin/cli.py + its tests/test_cli.py for plugin groups.
Pattern
from __future__ import annotations
from typing import Annotated
import typer
from nemo_helix_plugin.cli import create_typer_app
from nemo_helix_plugin.cli_codegen import handle_code_generation
from nemo_helix_plugin.cli_error_handling import handle_errors
from nemo_helix_plugin.cli_options import (
AllPagesOption,
EntityOutputFormatOption,
ListOutputFormatOption,
WorkspaceOption,
)
from nemo_helix_plugin.cli_output import Column, format_output
from nemo_helix_plugin.cli_pagination import PaginationType, collect_offset_pages, warn_if_more_pages
from nemo_helix_plugin.cli_state import cli_state, resolve_cli_workspace, resolve_output_format
from nemo_helix_plugin.cli_warnings import collect_warnings
from nemo_helix_plugin.secrets.client import SecretsClient
from nemo_helix_plugin.secrets.types import ListSecretsQueryParams
app = create_typer_app(name="secrets", help="Manage secrets.")
@app.command("get")
@collect_warnings
@handle_errors
def retrieve_secrets(
ctx: typer.Context,
name: Annotated[str, typer.Argument()],
workspace: WorkspaceOption = None,
output_format: EntityOutputFormatOption = None,
) -> None:
"""Retrieve a secret by its name."""
state = cli_state(ctx)
resolved_output_format = resolve_output_format(ctx, output_format)
kwargs = {"name": name, "workspace": resolve_cli_workspace(ctx, workspace)}
if handle_code_generation(SecretsClient, "get_secret", kwargs, resolved_output_format, state):
return
format_output(state.typed_client(SecretsClient).get_secret(**kwargs), output_format=resolved_output_format)
@app.command("list")
@collect_warnings
@handle_errors
def list_secrets(
ctx: typer.Context,
workspace: WorkspaceOption = None,
all_pages: AllPagesOption = False,
output_format: ListOutputFormatOption = None,
) -> None:
state = cli_state(ctx)
resolved_output_format = resolve_output_format(ctx, output_format)
query_params: ListSecretsQueryParams = {...} # omit keys the user did not set
kwargs = {"workspace": resolve_cli_workspace(ctx, workspace), "query_params": query_params}
if handle_code_generation(
SecretsClient, "list_secrets", kwargs, resolved_output_format, state,
result="all-pages" if all_pages else "list",
):
return
items = collect_offset_pages(state.typed_client(SecretsClient).list_secrets(**kwargs), all_pages=all_pages)
format_output(items, is_list=True, output_format=resolved_output_format, output_columns=[Column("name"), ...])
if not all_pages:
warn_if_more_pages(items, PaginationType.PAGE_NUMBER)Existing core commands still import some of these from their old nemo_helix_ext.cli.core.* paths,
which re-export them. New code imports from nemo_helix_plugin.
Conventions that keep the surface consistent:
- Client:
cli_state(ctx).typed_client(XClient)derives the service client from the CLI's sharedNemoClient(base URL, auth, token refresh). Never construct clients from config or env inside a command, and never add a per-command--base-urlor--cluster: the platform comes from the globalnemo --base-url/nemo --context. - Groups: build every group with
create_typer_app(...), nottyper.Typer(...). Undernemothe host also renders it withNhxGroup. - Options: use the shared aliases (
WorkspaceOption,ListOutputFormatOption, ...). Output format is always--output-format/--output/-f; do not add other spellings or reuse-ffor another option. Resolve withresolve_output_format(ctx, output_format)and name the resultresolved_output_format(ty otherwise narrows the option Literal). - Every
list/getcommand takes--output-formatand prints withformat_output. - Request bodies are the typed request models from
nemo_helix_plugin.<area>.types. Build them from user input withbuild_request_body(Model, data)(rejects unknown fields; only provided fields are sent), or from only the fields the user set. Query params are theTypedDicts intypes.py. - Output: single-entity responses go straight into
format_output(it unwrapsNemoResponse). Entities parsed by typed clients keep theirid/created_at/... metadata. List commands pass defaultoutput_columnsso tables are readable. -f code:handle_code_generation(XClient, "<method>", kwargs, ..., result="entity"|"list"|"all-pages"|"none"|"binary"); pass the same kwargs you call the client with. Reject options the snippet cannot express (for example--waitwith-f code) with a usage error rather than printing a misleading snippet.- Errors: let
nemo_helix_plugin.client.errorspropagate;@handle_errorsmaps them (API errors, including 404 and connection failures → exit 3; usage and client-side validation → exit 2). Raiseclick.BadParameterfor bad input andclick.ClickExceptionfor expected failures. - Warnings: decorate commands with
@collect_warningssoadd_warning(pagination, truncation) warnings are printed.
Register the group
Core group: add a TopLevelEntry to commands/manifest_registry.py (help must equal the Typer app's
help string; tests/cli/test_app.py::test_manifest_help_matches_loaded_manual_entry checks this).
Plugin-hosted group: subclass nemo_helix_plugin.cli.NemoCLI in the owning package and register it
under [project.entry-points."nemo.cli"] in that package's pyproject.toml (see
plugins/example-plugin). Run uv sync --frozen --all-packages so the entry point is installed. A plugin
group with the same name as a built-in core group replaces it.
Missing typed endpoint
If the typed client lacks a route, add it to nemo_helix_plugin/<area>/{endpoints,types,client}.py
(or the plugin's own types/endpoints.py + client.py) mirroring the server's FastAPI route exactly,
with tests.
Testing
Required for a new or changed group:
- Wire-level unit tests: drive the real Typer app against a typed client over
httpx.MockTransportand assert method, path, query string, exact JSON body, stdout,--all-pages, error mapping, and that-f codesends nothing and emits typed-client code.- Core groups (
tests/cli/commands/test_<group>.py): passCLIContext(_client=NemoClient(... http_client=httpx.Client(transport=httpx.MockTransport(recorder)))). - Plugin groups: pass a stand-in
CLIStatewhosetyped_clientbuilds the client over the mock transport, or mount the app under anNhxGrouproot with the realCLIContextto test what users see.
- Core groups (
- Integration tests (core,
tests/cli/integration/test_<group>.py): therunnerfixture injects aNemoClientbacked by the in-process ASGI app fromnhx.testing; add the service class tocreate_test_client(...)intests/cli/integration/conftest.pyif it is not hosted yet.
uv run --frozen pytest packages/nemo_helix_ext/tests/cli -q
uv run --frozen pytest packages/nemo_helix_plugin/tests -qTroubleshooting
- Group help mismatch: the manifest
helpand the Typer apphelpmust be identical. - "Missing workspace" exit 2 when a workspace is configured: the command passed
workspace=Noneto an endpoint whose path has no{workspace}default; resolve it withresolve_cli_workspace(ctx, workspace). -f codesnippet does not compile: the kwargs contain a value_render_valuecannot render; extendnemo_helix_plugin/cli_codegen.py(it handles pydantic models,RootModel, enums,SecretStrmasking, datetimes, dicts, lists) and add acompile()test inpackages/nemo_helix_plugin/tests/test_cli_codegen.py.RuntimeError: No NeMo Helix CLI state on this context: a test invoked the command withoutobj=; pass a CLI state.- Command needs a helper the typed client lacks: check
packages/filesets/src/filesets/transfer.py(fileset upload/download/list/delete) before adding a route.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

