From 88b3ae68220fa3c044c9d6ceed0355fa59d588eb Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Tue, 12 May 2026 14:32:26 -0700 Subject: [PATCH 1/3] feat(inference): add inference get command --- README.md | 4 ++ docs/get-started/quickstart.md | 4 ++ docs/inference/switch-inference-providers.md | 18 +++-- docs/reference/cli-selection-guide.md | 9 +-- docs/reference/commands.md | 10 +++ src/commands/inference/get.ts | 16 +++++ src/lib/actions/inference-get.test.ts | 65 +++++++++++++++++++ src/lib/actions/inference-get.ts | 65 +++++++++++++++++++ src/lib/actions/root-help.ts | 1 + src/lib/cli/oclif-dispatch.test.ts | 8 ++- src/lib/cli/oclif-dispatch.ts | 2 + .../global-oclif-command-adapters.test.ts | 15 +++++ src/lib/commands/inference/get.ts | 36 ++++++++++ src/lib/inference/live.test.ts | 49 ++++++++++++++ src/lib/inference/live.ts | 56 ++++++++++++++++ src/lib/list-command-deps.ts | 11 ++-- src/lib/onboard.ts | 3 +- src/lib/status-command-deps.ts | 16 ++--- test/cli.test.ts | 42 ++++++++++++ test/onboard.test.ts | 11 ++++ 20 files changed, 411 insertions(+), 30 deletions(-) create mode 100644 src/commands/inference/get.ts create mode 100644 src/lib/actions/inference-get.test.ts create mode 100644 src/lib/actions/inference-get.ts create mode 100644 src/lib/commands/inference/get.ts create mode 100644 src/lib/inference/live.test.ts create mode 100644 src/lib/inference/live.ts diff --git a/README.md b/README.md index 28732616ebc..e7ba98b42d5 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,10 @@ Status: nemoclaw my-assistant status Logs: nemoclaw my-assistant logs --follow ────────────────────────────────────────────────── +To change settings later: + Model: nemoclaw inference get + nemoclaw inference set --model --provider --sandbox my-assistant + [INFO] === Installation complete === ``` diff --git a/docs/get-started/quickstart.md b/docs/get-started/quickstart.md index a40d2757bb6..58cffb92f86 100644 --- a/docs/get-started/quickstart.md +++ b/docs/get-started/quickstart.md @@ -322,6 +322,10 @@ Status: nemoclaw my-gpt-claw status Logs: nemoclaw my-gpt-claw logs --follow ────────────────────────────────────────────────── +To change settings later: + Model: nemoclaw inference get + nemoclaw inference set --model --provider --sandbox my-gpt-claw + [INFO] === Installation complete === ``` diff --git a/docs/inference/switch-inference-providers.md b/docs/inference/switch-inference-providers.md index e758b243931..e8059769c37 100644 --- a/docs/inference/switch-inference-providers.md +++ b/docs/inference/switch-inference-providers.md @@ -30,7 +30,7 @@ No restart is required. ## Prerequisites - A running NemoClaw sandbox. -- The OpenShell CLI on your `PATH`. +- The OpenShell CLI on your `PATH`, which NemoClaw uses under the hood. ## Switch to a Different Model @@ -185,19 +185,25 @@ $ nemoclaw onboard --resume --recreate-sandbox ## Verify the Active Model -Run the status command to confirm the change: +Run the inference command to confirm the live gateway route: ```console -$ nemoclaw status +$ nemoclaw inference get +``` + +Add `--json` for machine-readable output: + +```console +$ nemoclaw inference get --json ``` -Add the `--json` flag for machine-readable output: +Run the status command when you also need sandbox, service, and messaging health: ```console -$ nemoclaw status --json +$ nemoclaw status ``` -The output includes the active provider, model, and endpoint. +The status output includes the active provider, model, and endpoint with the rest of the sandbox state. ## Notes diff --git a/docs/reference/cli-selection-guide.md b/docs/reference/cli-selection-guide.md index 8886c082e2b..2a657a9fab1 100644 --- a/docs/reference/cli-selection-guide.md +++ b/docs/reference/cli-selection-guide.md @@ -99,12 +99,6 @@ Use `openshell` when the docs explicitly call for a live OpenShell gateway opera $ openshell term ``` -- Inspect the live gateway inference route: - - ```console - $ openshell inference get -g nemoclaw - ``` - - Manage dashboard or service port forwards: ```console @@ -181,9 +175,10 @@ Approved endpoints are session-scoped unless you also add them to the policy thr ### Change Models or Providers -Use the NemoClaw command for model or provider switches so the OpenShell route and the running agent config stay consistent: +Use the NemoClaw commands for model or provider inspection and switches so the OpenShell route and the running agent config stay consistent: ```console +$ nemoclaw inference get $ nemoclaw inference set --provider nvidia-prod --model nvidia/nemotron-3-super-120b-a12b ``` diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 0f0b0eb6738..865cd816729 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -906,6 +906,16 @@ $ nemoclaw status $ nemoclaw status --json ``` +### `nemoclaw inference get` + +Show the active live inference provider and model from the NemoClaw-managed OpenShell gateway. +Use this command when you want the direct runtime route without the rest of the sandbox status output. + +```console +$ nemoclaw inference get +$ nemoclaw inference get --json +``` + ### `nemoclaw inference set` Switch the active inference provider or model for a NemoClaw-managed OpenClaw or Hermes sandbox. diff --git a/src/commands/inference/get.ts b/src/commands/inference/get.ts new file mode 100644 index 00000000000..864e9eab171 --- /dev/null +++ b/src/commands/inference/get.ts @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import Command from "../../lib/commands/inference/get"; +import { withCommandDisplay } from "../../lib/cli/command-display"; + +export default withCommandDisplay(Command, [ + { + usage: "nemoclaw inference get", + description: "Show the active inference provider and model", + flags: "[--json]", + group: "Services", + scope: "global", + order: 36, + }, +]); diff --git a/src/lib/actions/inference-get.test.ts b/src/lib/actions/inference-get.test.ts new file mode 100644 index 00000000000..6454ee81eb0 --- /dev/null +++ b/src/lib/actions/inference-get.test.ts @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../adapters/openshell/runtime", () => ({ + captureOpenshell: vi.fn(), +})); + +vi.mock("../inference/local", () => ({ + DEFAULT_OLLAMA_MODEL: "llama3.1", +})); + +import { runInferenceGet, type InferenceGetDeps } from "./inference-get"; + +function createDeps(output: string, status = 0): InferenceGetDeps & { + log: ReturnType; + captureOpenshell: ReturnType; +} { + const captureOpenshell = vi.fn(() => ({ status, output })); + const log = vi.fn(); + return { + captureOpenshell: captureOpenshell as unknown as InferenceGetDeps["captureOpenshell"] & + ReturnType, + log: log as unknown as InferenceGetDeps["log"] & ReturnType, + }; +} + +describe("runInferenceGet", () => { + it("prints the live provider and model", async () => { + const deps = createDeps("Gateway inference:\n Provider: nvidia-prod\n Model: nvidia/model\n"); + + await expect(runInferenceGet({}, deps)).resolves.toEqual({ + provider: "nvidia-prod", + model: "nvidia/model", + }); + + expect(deps.captureOpenshell).toHaveBeenCalledWith( + ["inference", "get", "-g", "nemoclaw"], + expect.objectContaining({ ignoreError: true }), + ); + expect(deps.log.mock.calls.map(([line]) => line)).toEqual([ + "Provider: nvidia-prod", + "Model: nvidia/model", + ]); + }); + + it("supports JSON output", async () => { + const deps = createDeps("Gateway inference:\n Provider: openai-api\n Model: gpt-5.4\n"); + + await runInferenceGet({ json: true }, deps); + + expect(JSON.parse(deps.log.mock.calls[0][0])).toEqual({ + provider: "openai-api", + model: "gpt-5.4", + }); + }); + + it("fails when no route is configured", async () => { + const deps = createDeps("Gateway inference:\n\n Not configured\n"); + + await expect(runInferenceGet({}, deps)).rejects.toThrow(/not configured/); + expect(deps.log).not.toHaveBeenCalled(); + }); +}); diff --git a/src/lib/actions/inference-get.ts b/src/lib/actions/inference-get.ts new file mode 100644 index 00000000000..67de5571ffd --- /dev/null +++ b/src/lib/actions/inference-get.ts @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { captureOpenshell } from "../adapters/openshell/runtime"; +import { OPENSHELL_PROBE_TIMEOUT_MS } from "../adapters/openshell/timeouts"; +import { getLiveGatewayInference } from "../inference/live"; + +export interface InferenceGetOptions { + json?: boolean; +} + +export interface InferenceGetResult { + provider: string | null; + model: string | null; +} + +export interface InferenceGetDeps { + captureOpenshell: typeof captureOpenshell; + log: (message?: string) => void; +} + +export class InferenceGetError extends Error { + constructor( + message: string, + readonly exitCode = 1, + ) { + super(message); + this.name = "InferenceGetError"; + } +} + +function defaultDeps(): InferenceGetDeps { + return { + captureOpenshell, + log: console.log, + }; +} + +export async function runInferenceGet( + options: InferenceGetOptions = {}, + deps: InferenceGetDeps = defaultDeps(), +): Promise { + const result = getLiveGatewayInference(deps.captureOpenshell, { + timeout: OPENSHELL_PROBE_TIMEOUT_MS, + }); + if (result.status !== 0) { + throw new InferenceGetError("OpenShell inference route lookup failed.", result.status || 1); + } + if (!result.inference) { + throw new InferenceGetError("OpenShell inference route is not configured."); + } + + const payload = { + provider: result.inference.provider, + model: result.inference.model, + }; + if (options.json) { + deps.log(JSON.stringify(payload, null, 2)); + } else { + deps.log(`Provider: ${payload.provider ?? "unknown"}`); + deps.log(`Model: ${payload.model ?? "unknown"}`); + } + + return payload; +} diff --git a/src/lib/actions/root-help.ts b/src/lib/actions/root-help.ts index 73909cf0538..8807a76456b 100644 --- a/src/lib/actions/root-help.ts +++ b/src/lib/actions/root-help.ts @@ -73,6 +73,7 @@ export function help(): void { lines.push(""); lines.push(` ${G}Reconfiguration (after onboard):${R}`); lines.push( + ` ${D}• Check inference route: nemoclaw inference get${R}`, ` ${D}• Change inference model: nemoclaw inference set --model --provider ${R}`, ); lines.push(` ${D}• Add network presets: use the policy-add command on your sandbox${R}`); diff --git a/src/lib/cli/oclif-dispatch.test.ts b/src/lib/cli/oclif-dispatch.test.ts index 09deb52a939..5abeffe5e2c 100644 --- a/src/lib/cli/oclif-dispatch.test.ts +++ b/src/lib/cli/oclif-dispatch.test.ts @@ -27,6 +27,11 @@ describe("resolveGlobalOclifDispatch", () => { commandId: "inference:set", args: ["--provider", "nvidia-prod"], }); + expect(resolveGlobalOclifDispatch("inference", ["get", "--json"])).toEqual({ + kind: "oclif", + commandId: "inference:get", + args: ["--json"], + }); expect(resolveGlobalOclifDispatch("--version", [])).toEqual({ kind: "oclif", commandId: "root:version", @@ -44,9 +49,10 @@ describe("resolveGlobalOclifDispatch", () => { kind: "usageError", lines: ["tunnel "], }); - expect(resolveGlobalOclifDispatch("inference", ["get"])).toEqual({ + expect(resolveGlobalOclifDispatch("inference", ["bogus"])).toEqual({ kind: "usageError", lines: [ + "inference get [--json]", "inference set --provider --model [--sandbox ] [--no-verify]", ], }); diff --git a/src/lib/cli/oclif-dispatch.ts b/src/lib/cli/oclif-dispatch.ts index 12aee762408..9b346deda63 100644 --- a/src/lib/cli/oclif-dispatch.ts +++ b/src/lib/cli/oclif-dispatch.ts @@ -119,10 +119,12 @@ export function resolveGlobalOclifDispatch(cmd: string, args: string[]): Dispatc if (cmd === "inference") { const sub = args[0]; + if (sub === "get") return oclif("inference:get", args.slice(1)); if (sub === "set") return oclif("inference:set", args.slice(1)); return { kind: "usageError", lines: [ + "inference get [--json]", "inference set --provider --model [--sandbox ] [--no-verify]", ], }; diff --git a/src/lib/commands/global-oclif-command-adapters.test.ts b/src/lib/commands/global-oclif-command-adapters.test.ts index 911336ca773..200508ee6b3 100644 --- a/src/lib/commands/global-oclif-command-adapters.test.ts +++ b/src/lib/commands/global-oclif-command-adapters.test.ts @@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({ renderSandboxInventoryText: vi.fn(), runBackupAllAction: vi.fn(), runGarbageCollectImagesAction: vi.fn(), + runInferenceGet: vi.fn(), runInferenceSet: vi.fn(), runOnboardAction: vi.fn(), runSetupAction: vi.fn(), @@ -50,6 +51,14 @@ vi.mock("../actions/inference-set", () => ({ runInferenceSet: mocks.runInferenceSet, })); +vi.mock("../actions/inference-get", () => ({ + InferenceGetError: class InferenceGetError extends Error { + exitCode = 1; + }, + runInferenceGet: mocks.runInferenceGet, +})); + +import InferenceGetCommand from "./inference/get"; import InferenceSetCommand from "./inference/set"; import ListCommand from "./list"; import BackupAllCommand from "./maintenance/backup-all"; @@ -152,4 +161,10 @@ describe("global oclif command adapters", () => { noVerify: true, }); }); + + it("maps inference get flags into the inference action", async () => { + await InferenceGetCommand.run(["--json"], rootDir); + + expect(mocks.runInferenceGet).toHaveBeenCalledWith({ json: true }); + }); }); diff --git a/src/lib/commands/inference/get.ts b/src/lib/commands/inference/get.ts new file mode 100644 index 00000000000..a9689a90342 --- /dev/null +++ b/src/lib/commands/inference/get.ts @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Flags } from "@oclif/core"; + +import { + InferenceGetError, + runInferenceGet, +} from "../../actions/inference-get"; +import { NemoClawCommand } from "../../cli/nemoclaw-oclif-command"; + +export default class InferenceGetCommand extends NemoClawCommand { + static id = "inference:get"; + static strict = true; + static summary = "Show the active NemoClaw inference route"; + static description = "Read the live OpenShell inference route through the NemoClaw CLI."; + static usage = ["inference get [--json]"]; + static examples = ["<%= config.bin %> inference get", "<%= config.bin %> inference get --json"]; + static flags = { + json: Flags.boolean({ + description: "Print provider and model as JSON", + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(InferenceGetCommand); + try { + await runInferenceGet({ json: flags.json === true }); + } catch (error) { + if (error instanceof InferenceGetError) { + this.error(error.message, { exit: error.exitCode }); + } + throw error; + } + } +} diff --git a/src/lib/inference/live.test.ts b/src/lib/inference/live.test.ts new file mode 100644 index 00000000000..29a4c443cc0 --- /dev/null +++ b/src/lib/inference/live.test.ts @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, vi } from "vitest"; + +vi.mock("./local", () => ({ + DEFAULT_OLLAMA_MODEL: "llama3.1", +})); + +import { getLiveGatewayInference } from "./live"; + +describe("getLiveGatewayInference", () => { + it("prefers the managed nemoclaw gateway route", () => { + const capture = vi.fn((args: string[]) => { + expect(args).toEqual(["inference", "get", "-g", "nemoclaw"]); + return { + status: 0, + output: "Gateway inference:\n Provider: nvidia-prod\n Model: nvidia/model\n", + }; + }); + + expect(getLiveGatewayInference(capture)).toEqual({ + args: ["inference", "get", "-g", "nemoclaw"], + inference: { provider: "nvidia-prod", model: "nvidia/model" }, + output: "Gateway inference:\n Provider: nvidia-prod\n Model: nvidia/model", + status: 0, + }); + expect(capture).toHaveBeenCalledTimes(1); + }); + + it("falls back to legacy inference get when grouped lookup is unavailable", () => { + const capture = vi + .fn() + .mockReturnValueOnce({ status: 1, output: "" }) + .mockReturnValueOnce({ + status: 0, + output: "Gateway inference:\n Provider: openai-api\n Model: gpt-5.4\n", + }); + + expect(getLiveGatewayInference(capture).inference).toEqual({ + provider: "openai-api", + model: "gpt-5.4", + }); + expect(capture.mock.calls.map(([args]) => args)).toEqual([ + ["inference", "get", "-g", "nemoclaw"], + ["inference", "get"], + ]); + }); +}); diff --git a/src/lib/inference/live.ts b/src/lib/inference/live.ts new file mode 100644 index 00000000000..5cb3e51b961 --- /dev/null +++ b/src/lib/inference/live.ts @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { CaptureOpenshellResult } from "../adapters/openshell/client"; +import { stripAnsi } from "../adapters/openshell/client"; +import { parseGatewayInference, type GatewayInference } from "./config"; + +type CaptureLiveInference = ( + args: string[], + opts?: { ignoreError?: boolean; timeout?: number }, +) => Pick; + +export interface LiveGatewayInferenceResult { + args: string[]; + inference: GatewayInference | null; + output: string; + status: number | null; +} + +function hasGatewayInferenceSection(output: string): boolean { + return /^Gateway inference:\s*$/im.test(output); +} + +export function getLiveGatewayInference( + capture: CaptureLiveInference, + opts: { timeout?: number } = {}, +): LiveGatewayInferenceResult { + const attempts = [ + ["inference", "get", "-g", "nemoclaw"], + ["inference", "get"], + ]; + let last: LiveGatewayInferenceResult = { + args: attempts[0], + inference: null, + output: "", + status: 1, + }; + + for (const args of attempts) { + const result = capture(args, { ignoreError: true, timeout: opts.timeout }); + const output = stripAnsi(result.output || "").trim(); + const inference = parseGatewayInference(output); + last = { + args, + inference, + output, + status: result.status, + }; + + if (result.status === 0 && (inference || hasGatewayInferenceSection(output))) { + return last; + } + } + + return last; +} diff --git a/src/lib/list-command-deps.ts b/src/lib/list-command-deps.ts index 2bd9b99a422..b19cd7c0ffe 100644 --- a/src/lib/list-command-deps.ts +++ b/src/lib/list-command-deps.ts @@ -3,7 +3,7 @@ import * as onboardSession from "./state/onboard-session"; import type { ListSandboxesCommandDeps, SandboxEntry } from "./inventory"; -import { parseGatewayInference } from "./inference/config"; +import { getLiveGatewayInference } from "./inference/live"; import { OPENSHELL_PROBE_TIMEOUT_MS } from "./adapters/openshell/timeouts"; import { parseSshProcesses, createSystemDeps } from "./state/sandbox-session"; import { resolveOpenshell } from "./adapters/openshell/resolve"; @@ -73,12 +73,9 @@ export function buildListCommandDeps(): ListSandboxesCommandDeps { ), getLiveInference: () => { try { - return parseGatewayInference( - captureOpenshell(["inference", "get"], { - ignoreError: true, - timeout: OPENSHELL_PROBE_TIMEOUT_MS, - }).output, - ); + return getLiveGatewayInference(captureOpenshell, { + timeout: OPENSHELL_PROBE_TIMEOUT_MS, + }).inference; } catch { return null; } diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index e4b9f037986..da5b1d1bb28 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -10692,8 +10692,9 @@ function printDashboard( console.log(` ${"─".repeat(50)}`); console.log(""); console.log(" To change settings later:"); + console.log(` Model: ${cliName()} inference get`); console.log( - ` Model: ${cliName()} inference set --model --provider --sandbox ${sandboxName}`, + ` ${cliName()} inference set --model --provider --sandbox ${sandboxName}`, ); console.log(` Policies: ${cliName()} ${sandboxName} policy-add`); console.log(` Credentials: ${cliName()} credentials reset then ${cliName()} onboard`); diff --git a/src/lib/status-command-deps.ts b/src/lib/status-command-deps.ts index cb90578c5ef..82879aaedab 100644 --- a/src/lib/status-command-deps.ts +++ b/src/lib/status-command-deps.ts @@ -4,7 +4,7 @@ import { spawnSync } from "node:child_process"; -import { parseGatewayInference } from "./inference/config"; +import { getLiveGatewayInference } from "./inference/live"; import type { MessagingBridgeHealth, ShowStatusCommandDeps } from "./inventory"; import { backfillMessagingChannels, findAllOverlaps } from "./messaging-conflict"; import type { CaptureOpenshellResult } from "./adapters/openshell/client"; @@ -136,13 +136,13 @@ export function buildStatusCommandDeps(rootDir: string): ShowStatusCommandDeps { return { listSandboxes: () => registry.listSandboxes(), getLiveInference: () => - parseGatewayInference( - stripAnsi( - captureOpenshell(rootDir, ["inference", "get"], { - timeout: OPENSHELL_PROBE_TIMEOUT_MS, - }).output, - ), - ), + getLiveGatewayInference( + (args, opts) => + captureOpenshell(rootDir, args, { + timeout: opts?.timeout, + }), + { timeout: OPENSHELL_PROBE_TIMEOUT_MS }, + ).inference, showServiceStatus, getServiceStatuses, checkMessagingBridgeHealth: (sandboxName, channels) => diff --git a/test/cli.test.ts b/test/cli.test.ts index 99ca5e61205..ef362f08eae 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -557,6 +557,48 @@ describe("CLI dispatch", () => { expect(out).toMatch(/OpenClaw or Hermes\s+sandbox config/); }); + it("inference get reports the live NemoClaw gateway route", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-inference-get-")); + const localBin = path.join(home, "bin"); + fs.mkdirSync(localBin, { recursive: true }); + fs.writeFileSync( + path.join(localBin, "openshell"), + [ + "#!/usr/bin/env bash", + 'if [ "$1" = "inference" ] && [ "$2" = "get" ] && [ "$3" = "-g" ] && [ "$4" = "nemoclaw" ]; then', + " echo 'Gateway inference:'", + " echo ' Provider: nvidia-prod'", + " echo ' Model: nvidia/nemotron-3-super-120b-a12b'", + " exit 0", + "fi", + "exit 1", + ].join("\n"), + { mode: 0o755 }, + ); + + try { + const text = runWithEnv("inference get", { + HOME: home, + PATH: `${localBin}:${process.env.PATH || ""}`, + }); + expect(text.code).toBe(0); + expect(text.out).toContain("Provider: nvidia-prod"); + expect(text.out).toContain("Model: nvidia/nemotron-3-super-120b-a12b"); + + const json = runWithEnv("inference get --json", { + HOME: home, + PATH: `${localBin}:${process.env.PATH || ""}`, + }); + expect(json.code).toBe(0); + expect(JSON.parse(json.out)).toEqual({ + provider: "nvidia-prod", + model: "nvidia/nemotron-3-super-120b-a12b", + }); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); + it("list --json emits structured empty inventory", () => { const r = run("list --json"); expect(r.code).toBe(0); diff --git a/test/onboard.test.ts b/test/onboard.test.ts index 70f98ae1e65..c83b680db98 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -7712,6 +7712,17 @@ const { setupInference } = require(${onboardPath}); assert.equal(commands.length, 4); }); + it("prints NemoClaw inference commands in the post-onboard settings summary", () => { + const source = fs.readFileSync(path.join(repoRoot, "src", "lib", "onboard.ts"), "utf8"); + const summaryBlock = source.slice(source.indexOf('console.log(" To change settings later:");')); + assert.match(summaryBlock, /Model:\s+\$\{cliName\(\)\} inference get/); + assert.match( + summaryBlock, + /inference set --model --provider --sandbox \$\{sandboxName\}/, + ); + assert.doesNotMatch(summaryBlock, /openshell inference (get|set)/); + }); + it("accepts gateway inference output that omits the Route line", () => { const repoRoot = path.join(import.meta.dirname, ".."); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-inference-route-")); From b674b2814b60e4b9817867537370b4e09ac8ede8 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Tue, 12 May 2026 14:50:09 -0700 Subject: [PATCH 2/3] fix(inference): address review feedback --- src/lib/actions/root-help.ts | 4 ++-- src/lib/onboard.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/actions/root-help.ts b/src/lib/actions/root-help.ts index 8807a76456b..8f0fca371ae 100644 --- a/src/lib/actions/root-help.ts +++ b/src/lib/actions/root-help.ts @@ -73,8 +73,8 @@ export function help(): void { lines.push(""); lines.push(` ${G}Reconfiguration (after onboard):${R}`); lines.push( - ` ${D}• Check inference route: nemoclaw inference get${R}`, - ` ${D}• Change inference model: nemoclaw inference set --model --provider ${R}`, + ` ${D}• Check inference route: ${CLI_NAME} inference get${R}`, + ` ${D}• Change inference model: ${CLI_NAME} inference set --model --provider ${R}`, ); lines.push(` ${D}• Add network presets: use the policy-add command on your sandbox${R}`); lines.push( diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index da5b1d1bb28..1fd1162cdd9 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -10692,9 +10692,8 @@ function printDashboard( console.log(` ${"─".repeat(50)}`); console.log(""); console.log(" To change settings later:"); - console.log(` Model: ${cliName()} inference get`); console.log( - ` ${cliName()} inference set --model --provider --sandbox ${sandboxName}`, + ` Model: ${cliName()} inference get\n ${cliName()} inference set --model --provider --sandbox ${sandboxName}`, ); console.log(` Policies: ${cliName()} ${sandboxName} policy-add`); console.log(` Credentials: ${cliName()} credentials reset then ${cliName()} onboard`); From ea0e2e2893a6692505b087c5889f627e822779f3 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Tue, 12 May 2026 15:07:51 -0700 Subject: [PATCH 3/3] fix(cli): update command registry expectations --- src/lib/cli/command-registry.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/cli/command-registry.test.ts b/src/lib/cli/command-registry.test.ts index 242da5b07c9..bdd4c2c5884 100644 --- a/src/lib/cli/command-registry.test.ts +++ b/src/lib/cli/command-registry.test.ts @@ -17,10 +17,10 @@ import type { CommandDef } from "./command-registry"; describe("command-registry", () => { describe("COMMANDS array", () => { - it("should contain exactly 57 commands", () => { - // 25 global (20 visible + 5 hidden help/version aliases) + it("should contain exactly 58 commands", () => { + // 26 global (21 visible + 5 hidden help/version aliases) // 32 sandbox (26 visible + 6 hidden shields/config) - expect(COMMANDS).toHaveLength(57); + expect(COMMANDS).toHaveLength(58); }); it("should have no duplicate usage strings", () => { @@ -39,9 +39,9 @@ describe("command-registry", () => { }); describe("globalCommands()", () => { - it("should return exactly 25 entries", () => { - // 20 visible + 5 hidden (help, --help, -h, --version, -v) - expect(globalCommands()).toHaveLength(25); + it("should return exactly 26 entries", () => { + // 21 visible + 5 hidden (help, --help, -h, --version, -v) + expect(globalCommands()).toHaveLength(26); }); it("every entry has scope global", () => { @@ -65,10 +65,10 @@ describe("command-registry", () => { }); describe("visibleCommands()", () => { - it("should exclude 11 hidden commands (46 visible)", () => { + it("should exclude 11 hidden commands (47 visible)", () => { // 5 hidden global (help, --help, -h, --version, -v) + // 6 hidden sandbox (shields×3, config get/set/rotate-token) - expect(visibleCommands()).toHaveLength(46); + expect(visibleCommands()).toHaveLength(47); }); it("no visible command has hidden=true", () => {