Skip to content
3 changes: 2 additions & 1 deletion docs/reference/cli-selection-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ openshell sandbox exec -n my-assistant -- cat /tmp/gateway.log
### Check Health or Logs

Use `$$nemoclaw <name> status` and `$$nemoclaw <name> logs` first.
They combine NemoClaw registry data, OpenShell state, OpenClaw process health, inference health, policy details, and messaging-channel warnings.
They combine NemoClaw registry data, OpenShell state, OpenClaw process health, inference health, policy details, and messaging-channel warnings for that sandbox.
Use `$$nemoclaw status` only for the global all-sandbox and host-service overview.

Use `openshell sandbox list`, `openshell sandbox get`, `openshell logs <name> -n 20`, or `openshell doctor check` when debugging lower-level OpenShell behavior.
When using `openshell logs` directly, `-n <lines>` controls the line count; use `--tail` only when you want live OpenShell log streaming.
Expand Down
8 changes: 6 additions & 2 deletions docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,9 @@ Terminal agents do not have a gateway runtime and fail as unsupported.

### `nemohermes <name> status`

Show sandbox status, health, and inference configuration.
Show sandbox-scoped status, health, and inference configuration for one registered sandbox.
Use this form when you care about a specific sandbox's live OpenShell state, agent runtime, inference health, GPU proof, permissions, and recovery hints.
Do not pass a sandbox name to `nemohermes status`; that command is the global all-sandbox/service overview.

Pass `--json` to emit a structured per-sandbox report instead of the text renderer.
The JSON output includes at least `schemaVersion`, `name`, `found`, `model`, `provider`, `phase`, `gatewayState`, `inferenceHealth`, `rpcIssue`, `hostGpuDetected`, `sandboxGpuEnabled`, `sandboxGpuMode`, `sandboxGpuDevice`, `openshellDriver`, `openshellVersion`, `policies`, `failureLayer`, `terminalRuntimeHealth`, and `dockerPaused`.
Expand Down Expand Up @@ -1586,7 +1588,9 @@ This command remains as a compatibility alias to `nemohermes tunnel stop`.

### `nemohermes status`

Show the sandbox list and the status of host auxiliary services (for example cloudflared).
Show the global sandbox list and the status of host auxiliary services (for example cloudflared).
This command is host-wide. It summarizes registered sandboxes, the default sandbox's live inference route, messaging overlap warnings, gateway health, and host services.
Use `nemohermes <name> status` when you need one sandbox's live health and recovery guidance.
Pass `--json` for machine-readable output with registered sandboxes, service state, inference routes, and messaging health.
For each listed sandbox, the text output includes the configured inference provider and model plus whether an active SSH session is connected.
Host-service PID lookup honors `NEMOCLAW_SANDBOX_NAME`, then `NEMOCLAW_SANDBOX`, then `SANDBOX_NAME`, then the registry default.
Expand Down
8 changes: 6 additions & 2 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,9 @@ Terminal agents do not have a gateway runtime and fail as unsupported.

### `$$nemoclaw <name> status`

Show sandbox status, health, and inference configuration.
Show sandbox-scoped status, health, and inference configuration for one registered sandbox.
Use this form when you care about a specific sandbox's live OpenShell state, agent runtime, inference health, GPU proof, permissions, and recovery hints.
Do not pass a sandbox name to `$$nemoclaw status`; that command is the global all-sandbox/service overview.

Pass `--json` to emit a structured per-sandbox report instead of the text renderer.
The JSON output includes at least `schemaVersion`, `name`, `found`, `model`, `provider`, `phase`, `gatewayState`, `inferenceHealth`, `rpcIssue`, `hostGpuDetected`, `sandboxGpuEnabled`, `sandboxGpuMode`, `sandboxGpuDevice`, `openshellDriver`, `openshellVersion`, `policies`, `failureLayer`, `terminalRuntimeHealth`, and `dockerPaused`.
Expand Down Expand Up @@ -1964,7 +1966,9 @@ This command remains as a compatibility alias to `$$nemoclaw tunnel stop`.

### `$$nemoclaw status`

Show the sandbox list and the status of host auxiliary services (for example cloudflared).
Show the global sandbox list and the status of host auxiliary services (for example cloudflared).
This command is host-wide. It summarizes registered sandboxes, the default sandbox's live inference route, messaging overlap warnings, gateway health, and host services.
Use `$$nemoclaw <name> status` when you need one sandbox's live health and recovery guidance.
Pass `--json` for machine-readable output with registered sandboxes, service state, inference routes, and messaging health.
For each listed sandbox, the text output includes the configured inference provider and model plus whether an active SSH session is connected.
Host-service PID lookup honors `NEMOCLAW_SANDBOX_NAME`, then `NEMOCLAW_SANDBOX`, then `SANDBOX_NAME`, then the registry default.
Expand Down
6 changes: 4 additions & 2 deletions src/commands/sandbox/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export default class SandboxStatusCommand extends NemoClawCommand {
static id = "sandbox:status";
static strict = true;
static enableJsonFlag = true;
static summary = "Sandbox health and NIM status";
static description = "Show sandbox health, OpenShell gateway state, and local NIM status.";
static summary = "Show one sandbox's health and runtime status";
static description =
"Show one sandbox's health, OpenShell gateway state, inference status, and local NIM status. Use global `status` for the all-sandbox/service overview.";
static usage = ["<name> [--json]"];
static examples = [
"<%= config.bin %> alpha status",
"<%= config.bin %> sandbox status alpha",
"<%= config.bin %> sandbox status alpha --json",
];
Expand Down
7 changes: 4 additions & 3 deletions src/commands/status.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { getStatusReport, showStatusCommand } from "../lib/inventory/index";
import { NemoClawCommand } from "../lib/cli/nemoclaw-oclif-command";
import { getStatusReport, showStatusCommand } from "../lib/inventory/index";
import { buildStatusCommandDeps } from "../lib/status-command-deps";

export default class StatusCommand extends NemoClawCommand {
static id = "status";
static strict = true;
static enableJsonFlag = true;
static summary = "Show sandbox list and service status";
static description = "Show registered sandboxes, live inference, services, and messaging health.";
static summary = "Show global sandbox and host service status";
static description =
"Show the global overview across registered sandboxes, live inference, host services, and messaging health. Use `<name> status` for one sandbox.";
static usage = ["status [--json]"];
static examples = ["<%= config.bin %> status", "<%= config.bin %> status --json"];
static flags = {};
Expand Down
6 changes: 6 additions & 0 deletions src/lib/actions/root-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export function help(): void {
lines.push("");
lines.push(` ${B}${G}${CLI_DISPLAY_NAME}${R} ${D}v${getVersion()}${R}`);
lines.push(` ${D}Deploy more secure, always-on AI assistants with a single command.${R}`);
lines.push(
` ${D}Global commands run without a sandbox-name prefix; sandbox commands start with a sandbox name.${R}`,
);
lines.push(
` ${D}Use \`${CLI_NAME} status\` for the global overview, and \`${CLI_NAME} <name> status\` for one sandbox.${R}`,
);

for (const [group, cmds] of grouped) {
lines.push("");
Expand Down
1 change: 1 addition & 0 deletions src/lib/actions/sandbox/status-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ describe("showSandboxStatus flow", () => {
await expect(harness.showSandboxStatus("alpha")).resolves.toBeUndefined();

const output = harness.logSpy.mock.calls.map((call) => String(call[0])).join("\n");
expect(output).toContain("Sandbox-scoped status for 'alpha'");
expect(output).toContain("Sandbox: alpha");
expect(output).toContain("Model: nvidia/nemotron-live");
expect(output).toContain("Inference: healthy");
Expand Down
1 change: 1 addition & 0 deletions src/lib/actions/sandbox/status-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export function printSandboxDetails(context: SandboxStatusTextContext): SandboxS
if (!sb) return { exitCode: null };

console.log("");
console.log(` Sandbox-scoped status for '${sb.name}':`);
console.log(` Sandbox: ${sb.name}`);
console.log(` Model: ${currentModel}`);
console.log(` Provider: ${currentProvider}`);
Expand Down
93 changes: 75 additions & 18 deletions src/lib/cli/public-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,25 @@ const {
globalCommandTokens,
sandboxActionTokens,
} = require("./command-registry");
import { normalizeArgv, suggestCommand, type NormalizedSandboxArgv } from "./argv-normalizer";

import {
type NormalizedArgv,
type NormalizedGlobalArgv,
type NormalizedSandboxArgv,
normalizeArgv,
suggestCommand,
} from "./argv-normalizer";
import { getRegisteredOclifCommandMetadata } from "./oclif-metadata";
import {
type PublicTranslationResult,
translatePublicGlobalArgv,
translatePublicSandboxArgv,
type PublicTranslationResult,
} from "./public-argv-translation";

// ── Global commands (derived from command registry) ──────────────

const GLOBAL_COMMANDS = globalCommandTokens();
const NATIVE_OCLIF_NAMESPACES = new Set(["internal", "sandbox"]);

type RegistryModule = typeof import("../state/registry");
type RegistryRecoveryModule = typeof import("../registry-recovery-action");
Expand Down Expand Up @@ -203,6 +211,34 @@ function printDispatchUsageError(
process.exit(1);
}

/** Returns the sandbox-like positional argument passed to global `status`, if one exists. */
function findGlobalStatusSandboxArgument(args: readonly string[]): string | null {
const positionals = args.filter((arg) => !["--json", "--help", "-h"].includes(arg));
if (positionals.some((arg) => arg.startsWith("-")) || positionals.length !== 1) return null;
if (GLOBAL_COMMANDS.has(positionals[0]) || NATIVE_OCLIF_NAMESPACES.has(positionals[0])) {
return null;
}
try {
validateName(positionals[0], "sandbox name");
return positionals[0];
} catch {
return null;
}
}

/** Prints the correction for `status <name>` and exits with the usage-error status code. */
function printGlobalStatusScopeHint(sandboxName: string, args: readonly string[]): never {
const helpRequested = hasHelpFlag(args);
const forwardedFlags = helpRequested ? ["--help"] : args.includes("--json") ? ["--json"] : [];
const flagSuffix = forwardedFlags.length > 0 ? ` ${forwardedFlags.join(" ")}` : "";
console.error(` '${CLI_NAME} status' shows the global sandbox/service overview.`);
console.error(` It does not take a sandbox name.`);
console.error("");
console.error(` Run: ${CLI_NAME} ${sandboxName} status${flagSuffix}`);
console.error(` Or for global JSON: ${CLI_NAME} status --json`);
process.exit(2);
}

async function recoverRequestedSandboxIfNeeded(
sandboxName: string,
action: string,
Expand Down Expand Up @@ -290,18 +326,7 @@ async function runPublicTranslationResult(

// ── Dispatch ─────────────────────────────────────────────────────

// eslint-disable-next-line complexity
export async function dispatchCli(argv: string[] = process.argv.slice(2)): Promise<void> {
if (argv[0] === "internal" || argv[0] === "sandbox") {
await runNativeOclifArgv(argv);
return;
}

const normalized = normalizeArgv(argv, {
globalCommands: GLOBAL_COMMANDS,
isSandboxConnectFlag: isPublicSandboxConnectFlag,
});

async function dispatchNormalizedArgv(normalized: NormalizedArgv, argv: string[]): Promise<void> {
if (normalized.kind === "rootHelp") {
await runDirectOclifCommand("root:help", []);
return;
Expand All @@ -313,12 +338,25 @@ export async function dispatchCli(argv: string[] = process.argv.slice(2)): Promi
}

if (normalized.kind === "global") {
await runPublicTranslationResult(
translatePublicGlobalArgv(normalized.command, normalized.args),
);
await dispatchGlobalArgv(normalized);
return;
}

await dispatchSandboxArgv(normalized, argv);
}

async function dispatchGlobalArgv(normalized: NormalizedGlobalArgv): Promise<void> {
if (normalized.command === "status") {
const sandboxName = findGlobalStatusSandboxArgument(normalized.args);
if (sandboxName) printGlobalStatusScopeHint(sandboxName, normalized.args);
}
await runPublicTranslationResult(translatePublicGlobalArgv(normalized.command, normalized.args));
}

async function dispatchSandboxArgv(
normalized: NormalizedSandboxArgv,
argv: string[],
): Promise<void> {
const cmd = normalized.sandboxName;
const rawArgsAfterCmd = argv.slice(1);
const requestedSandboxAction = normalized.action;
Expand Down Expand Up @@ -378,7 +416,10 @@ export async function dispatchCli(argv: string[] = process.argv.slice(2)): Promi
return;
}

// Unknown command — suggest
printUnknownSandboxOrCommand(cmd);
}

function printUnknownSandboxOrCommand(cmd: string): never {
console.error(` Unknown command: ${cmd}`);
console.error("");

Expand All @@ -400,3 +441,19 @@ export async function dispatchCli(argv: string[] = process.argv.slice(2)): Promi
console.error(` Run '${CLI_NAME} help' for usage.`);
process.exit(1);
}

/** Normalize public argv and route it to oclif or sandbox-first command handlers. */
export async function dispatchCli(argv: string[] = process.argv.slice(2)): Promise<void> {
if (argv[0] && NATIVE_OCLIF_NAMESPACES.has(argv[0])) {
await runNativeOclifArgv(argv);
return;
}

await dispatchNormalizedArgv(
normalizeArgv(argv, {
globalCommands: GLOBAL_COMMANDS,
isSandboxConnectFlag: isPublicSandboxConnectFlag,
}),
argv,
);
}
3 changes: 2 additions & 1 deletion src/lib/cli/public-display-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ const PUBLIC_DISPLAY_LAYOUT: Record<string, readonly PublicDisplayLayout[]> = {
{
group: "Sandbox Management",
order: 4,
description: "Sandbox health + NIM status",
description: "One sandbox's health, gateway, inference, and NIM status",
},
],
setup: [
Expand Down Expand Up @@ -477,6 +477,7 @@ const PUBLIC_DISPLAY_LAYOUT: Record<string, readonly PublicDisplayLayout[]> = {
{
group: "Services",
order: 36,
description: "Global sandbox and host service status",
flags: "[--json]",
},
],
Expand Down
1 change: 1 addition & 0 deletions src/lib/inventory/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ describe("inventory commands", () => {
log: (message = "") => lines.push(message),
});

expect(lines).toContain(" Global status (registered sandboxes and host services):");
expect(lines).toContain(" Sandboxes:");
// Default sandbox shows the live gateway model (#2369), annotated with
// the onboarded model when they differ.
Expand Down
3 changes: 2 additions & 1 deletion src/lib/inventory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,9 +453,10 @@ export function showStatusCommand(deps: ShowStatusCommandDeps): void {
const sandboxList = deps.listSandboxes();
const { sandboxes } = sandboxList;
const resolvedDefault = resolveDefaultSandboxName(() => sandboxList) ?? null;
log("");
log(" Global status (registered sandboxes and host services):");
if (sandboxes.length > 0) {
const live = deps.getLiveInference();
log("");
log(" Sandboxes:");
for (const sb of sandboxes) {
const isDefault = sb.name === resolvedDefault;
Expand Down
22 changes: 16 additions & 6 deletions test/cli-oclif-compatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,15 @@ describe("oclif compatibility dispatch", () => {

const runOclifArgv = vi.fn(async () => undefined);
const runOclifCommandById = vi.fn(async () => undefined);
const stderr: string[] = [];
const errorSpy = vi.spyOn(console, "error").mockImplementation((message = "") => {
stderr.push(String(message));
});
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((
code?: string | number | null,
) => {
throw new Error(`process.exit:${String(code)}`);
}) as never);

process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH = "1";
requireCache[runnerPath] = {
Expand All @@ -351,15 +360,16 @@ describe("oclif compatibility dispatch", () => {
delete require.cache[publicDispatchPath];
const { dispatchCli } = require(cliPath);

await dispatchCli(["status", "bogus"]);
await expect(dispatchCli(["status", "bogus"])).rejects.toThrow("process.exit:2");

expect(runOclifCommandById).toHaveBeenCalledWith(
"status",
["bogus"],
expect.objectContaining({ rootDir: process.cwd() }),
);
expect(exitSpy).toHaveBeenCalledWith(2);
expect(stderr.join("\n")).toContain("Run: nemoclaw bogus status");
expect(runOclifCommandById).not.toHaveBeenCalled();
expect(runOclifArgv).not.toHaveBeenCalled();

errorSpy.mockClear();
exitSpy.mockClear();
stderr.length = 0;
runOclifArgv.mockClear();
runOclifCommandById.mockClear();

Expand Down
Loading
Loading