Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/dev-tier-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const creds = require("../dist/lib/credentials/store.js");
const runner = require("../dist/lib/runner.js");
const registry = require("../dist/lib/state/registry.js");

creds.ensureApiKey = async () => {};
creds.ensureApiKey = async () => ({ kind: "credential", value: "dev-tier-selector" });
creds.getCredential = () => null;
creds.prompt = (msg) =>
new Promise((resolve) => {
Expand Down
34 changes: 31 additions & 3 deletions src/lib/credentials/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import { rejectSymlinksOnPath } from "../state/config-io";
const UNSAFE_HOME_PATHS = new Set(["/tmp", "/var/tmp", "/dev/shm", "/"]);

type CredentialInput = string | null | undefined;
export type CredentialPromptIntent =
| { kind: "credential"; value: string }
| { kind: "back" }
| { kind: "exit" }
| { kind: "help" };

// Credential env keys NemoClaw knows how to round-trip. listCredentialKeys()
// projects the in-process env through this set; entries not in the set are
Expand Down Expand Up @@ -126,6 +131,15 @@ export function normalizeCredentialValue(value: CredentialInput): string {
return value.replace(/\r/g, "").trim();
}

export function getCredentialPromptIntent(value: CredentialInput): CredentialPromptIntent {
const normalized = normalizeCredentialValue(value);
const navigation = normalized.toLowerCase();
if (navigation === "back") return { kind: "back" };
if (navigation === "exit" || navigation === "quit") return { kind: "exit" };
if (navigation === "?" || navigation === "help") return { kind: "help" };
return { kind: "credential", value: normalized };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Stage a credential for the current process. The OpenShell upsert that
* follows in onboarding (`openshell provider create/update --credential KEY`)
Expand Down Expand Up @@ -643,17 +657,24 @@ export function prompt(question: string, opts: { secret?: boolean } = {}): Promi
});
}

export async function readCredentialPrompt(
question: string,
promptImpl: typeof prompt = prompt,
): Promise<CredentialPromptIntent> {
return getCredentialPromptIntent(await promptImpl(question, { secret: true }));
}

/**
* Ensure `NVIDIA_API_KEY` is staged for this process. Returns immediately
* if it is already in env, otherwise prompts interactively (validating
* the `nvapi-` prefix) and stages the result. Onboarding registers the
* value with the OpenShell gateway later in the flow.
*/
export async function ensureApiKey(): Promise<void> {
export async function ensureApiKey(): Promise<CredentialPromptIntent> {
let key = getCredential("NVIDIA_API_KEY");
if (key) {
process.env.NVIDIA_API_KEY = key;
return;
return { kind: "credential", value: key };
}

console.log("");
Expand All @@ -668,7 +689,13 @@ export async function ensureApiKey(): Promise<void> {
console.log("");

while (true) {
key = normalizeCredentialValue(await prompt(" NVIDIA API Key: ", { secret: true }));
const input = getCredentialPromptIntent(await prompt(" NVIDIA API Key: ", { secret: true }));
if (input.kind === "help") {
console.log(" Type back to choose a different provider, or exit to quit.");
continue;
}
if (input.kind !== "credential") return input;
key = input.value;

if (!key) {
console.error(" NVIDIA API Key is required.");
Expand All @@ -689,4 +716,5 @@ export async function ensureApiKey(): Promise<void> {
console.log(" Key staged for the OpenShell gateway. It is held in process memory only;");
console.log(" onboarding registers it with the gateway and nothing is written to disk.");
console.log("");
return { kind: "credential", value: key };
}
22 changes: 14 additions & 8 deletions src/lib/inference/model-prompts.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { CLOUD_MODEL_OPTIONS, HERMES_PROVIDER_MODEL_OPTIONS } from "./config";
import {
BACK_TO_SELECTION,
type BackToSelection,
} from "../navigation";
import { isSafeModelId } from "../validation";
import { CLOUD_MODEL_OPTIONS, HERMES_PROVIDER_MODEL_OPTIONS } from "./config";
import { validateNvidiaEndpointModel } from "./provider-models";

// credentials.ts still uses CommonJS-style exports.
const { getCredential, prompt } = require("../credentials/store");

export const BACK_TO_SELECTION = "__NEMOCLAW_BACK_TO_SELECTION__";
export type { BackToSelection };
export { BACK_TO_SELECTION };
export type ModelPromptResult = string | BackToSelection;

export const REMOTE_MODEL_OPTIONS: Record<string, string[]> = {
openai: ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.4-pro-2026-03-05"],
Expand Down Expand Up @@ -40,7 +46,7 @@ export interface ModelPromptOptions {
validateNvidiaEndpointModelFn?: (model: string, apiKey: string) => PromptValidationResult;
cloudModelOptions?: Array<{ id: string; label: string }>;
remoteModelOptions?: Record<string, string[]>;
backToSelection?: string;
backToSelection?: BackToSelection;
/** Pre-fill this model ID as the default in interactive prompts. */
defaultModelId?: string;
/** Show only this many remote models in the first menu before offering Other. */
Expand Down Expand Up @@ -91,7 +97,7 @@ export async function promptManualModelId(
errorLabel: string,
validator: ((model: string) => PromptValidationResult) | null = null,
options: ModelPromptOptions = {},
): Promise<string> {
): Promise<ModelPromptResult> {
const deps = resolvePromptOptions(options);
while (true) {
const manual = await deps.promptFn(promptLabel);
Expand Down Expand Up @@ -123,7 +129,7 @@ export async function promptManualModelId(
}
}

export async function promptCloudModel(options: ModelPromptOptions = {}): Promise<string> {
export async function promptCloudModel(options: ModelPromptOptions = {}): Promise<ModelPromptResult> {
const deps = resolvePromptOptions(options);
const defaultModelId = options.defaultModelId ?? "";

Expand Down Expand Up @@ -180,7 +186,7 @@ export async function promptRemoteModel(
defaultModel: string,
validator: ((model: string) => PromptValidationResult) | null = null,
options: ModelPromptOptions = {},
): Promise<string> {
): Promise<ModelPromptResult> {
const deps = resolvePromptOptions(options);
const modelOptions = deps.remoteModelOptions[providerKey] || [];
const defaultIndex = modelOptions.indexOf(defaultModel);
Expand Down Expand Up @@ -242,7 +248,7 @@ async function promptFullRemoteModelList(
defaultModel: string,
validator: ((model: string) => PromptValidationResult) | null,
options: ModelPromptOptions,
): Promise<string> {
): Promise<ModelPromptResult> {
const deps = resolvePromptOptions(options);
const defaultIndex = Math.max(0, modelOptions.indexOf(defaultModel));

Expand Down Expand Up @@ -275,7 +281,7 @@ export async function promptInputModel(
defaultModel: string,
validator: ((model: string) => PromptValidationResult) | null = null,
options: ModelPromptOptions = {},
): Promise<string> {
): Promise<ModelPromptResult> {
const deps = resolvePromptOptions(options);
while (true) {
const value = await deps.promptFn(` ${label} model [${defaultModel}]: `);
Expand Down
9 changes: 9 additions & 0 deletions src/lib/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export const BACK_TO_SELECTION = Object.freeze({ kind: "NEMOCLAW_BACK_TO_SELECTION" });
export type BackToSelection = typeof BACK_TO_SELECTION;

export function isBackToSelection(value: unknown): value is BackToSelection {
return value === BACK_TO_SELECTION;
}
Loading
Loading