Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a3504c8
feat(cli): add connection info command to reprint the connection block
laitingsheng Jul 24, 2026
12d688d
refactor(cli): keep onboard entrypoint net-neutral for connection info
laitingsheng Jul 25, 2026
71e2076
Merge remote-tracking branch 'origin/main' into feat/connection-info-…
laitingsheng Jul 25, 2026
4ceac20
test(cli): behaviour-oriented issue-linked titles for connection info
laitingsheng Jul 25, 2026
5101fbe
Merge remote-tracking branch 'origin/main' into feat/connection-info-…
laitingsheng Jul 25, 2026
f0f2887
merge: resolve conflicts with main
github-actions[bot] Jul 26, 2026
39c9fec
Merge remote-tracking branch 'origin/main' into feat/connection-info-…
sandl99 Jul 28, 2026
3d98c44
feat(cli): enrich dashboard-url connection guidance
sandl99 Jul 28, 2026
67974cd
Merge remote-tracking branch 'origin/main' into feat/connection-info-…
sandl99 Jul 28, 2026
c578254
Merge branch 'main' into feat/connection-info-command
sandl99 Jul 28, 2026
1e17215
test(cli): isolate dashboard-url runtime state
sandl99 Jul 28, 2026
bf705fb
Merge remote-tracking branch 'origin/feat/connection-info-command' in…
sandl99 Jul 28, 2026
f56df0a
merge: refresh dashboard guidance with main
prekshivyas Jul 28, 2026
d1e8240
fix(cli): use invoked binary in dashboard guidance
prekshivyas Jul 28, 2026
5d9a241
test(cli): cover invoked dashboard binary
prekshivyas Jul 28, 2026
df9fc8d
merge: refresh dashboard guidance with main
prekshivyas Jul 28, 2026
21af1a6
merge: refresh dashboard guidance with main
laitingsheng Jul 30, 2026
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
5 changes: 4 additions & 1 deletion docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1612,7 +1612,8 @@ $$nemoclaw my-assistant dashboard-url
$$nemoclaw my-assistant dashboard-url --quiet
```

The default output includes a label and a warning.
The default output includes the terminal connection command and management commands shown after onboarding.
It also includes a label and a warning.
Pass `--quiet` or `-q` to print only the URL to stdout so scripts can capture it:

```bash
Expand All @@ -1630,6 +1631,8 @@ This warning applies when the command prints an OpenClaw tokenized URL.

Print the browser dashboard URL for a running Hermes sandbox.
Hermes manages dashboard sessions itself, so this command prints a plain URL without an OpenClaw `#token=` fragment.
The default output also includes terminal connection and management commands.
Pass `--quiet` or `-q` to print only the URL to stdout.
The built-in dashboard is forwarded on port `18789` by default.

```bash
Expand Down
80 changes: 80 additions & 0 deletions src/commands/sandbox/dashboard-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Config as OclifConfig } from "@oclif/core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import DashboardUrlCliCommand, {
resetDashboardUrlRuntimeBridgeFactoryForTest,
setDashboardUrlRuntimeBridgeFactoryForTest,
} from "./dashboard-url";

describe("dashboard-url CLI output", () => {
beforeEach(() => {
vi.restoreAllMocks();
setDashboardUrlRuntimeBridgeFactoryForTest(() => ({
fetchGatewayAuthTokenFromSandbox: () => "secret-token",
getSandbox: () => ({ agent: "openclaw", dashboardPort: 18789 }),
getAccessUrl: () => "http://127.0.0.1:18789",
}));
});

afterEach(() => {
resetDashboardUrlRuntimeBridgeFactoryForTest();
vi.restoreAllMocks();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("prints the authenticated URL with connection and management guidance (#7473)", async () => {
const output: string[] = [];
const errors: string[] = [];
vi.spyOn(console, "log").mockImplementation((message: string) => output.push(message));
vi.spyOn(console, "error").mockImplementation((message: string) => errors.push(message));

const previousExitCode = process.exitCode;
try {
await DashboardUrlCliCommand.run(["alpha"], process.cwd());

expect(output).toContain(" http://127.0.0.1:18789/#token=secret-token");
expect(output).toContain(" nemoclaw alpha connect");
expect(output).toContain(" Manage later");
expect(output).toContain(" Status: nemoclaw alpha status");
expect(output).toContain(" Logs: nemoclaw alpha logs --follow");
expect(errors.join("\n")).toContain("Treat this URL like a password");
} finally {
process.exitCode = previousExitCode;
}
});

it("uses the invoked CLI binary in every follow-up command (#7473)", async () => {
const output: string[] = [];
vi.spyOn(console, "log").mockImplementation((message: string) => output.push(message));
vi.spyOn(console, "error").mockImplementation(() => {});

const baseConfig = await OclifConfig.load(process.cwd());
const config = await OclifConfig.load({
root: process.cwd(),
pjson: {
...baseConfig.pjson,
oclif: { ...baseConfig.pjson.oclif, bin: "nemohermes" },
},
});

const previousExitCode = process.exitCode;
try {
await DashboardUrlCliCommand.run(["alpha"], config);

expect(output).toContain(" nemohermes alpha connect");
expect(output).toContain(" Status: nemohermes alpha status");
expect(output).toContain(" Logs: nemohermes alpha logs --follow");
expect(output).toContain(
" Model: nemohermes inference set --model <model> --provider <provider> --sandbox alpha",
);
expect(output).toContain(" Policies: nemohermes alpha policy add");
expect(output).toContain(
" Credentials: nemohermes credentials reset <KEY> && nemohermes onboard",
);
} finally {
process.exitCode = previousExitCode;
}
});
});
12 changes: 10 additions & 2 deletions src/commands/sandbox/dashboard-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type DashboardUrlRuntimeBridge = {
getAccessUrl?: (port: number) => string | null;
};

let runtimeBridgeFactory = (): DashboardUrlRuntimeBridge => {
const defaultDashboardUrlRuntimeBridgeFactory = (): DashboardUrlRuntimeBridge => {
const onboard = require("../../lib/onboard") as Pick<
DashboardUrlRuntimeBridge,
"fetchGatewayAuthTokenFromSandbox"
Expand Down Expand Up @@ -43,12 +43,18 @@ let runtimeBridgeFactory = (): DashboardUrlRuntimeBridge => {
};
};

let runtimeBridgeFactory = defaultDashboardUrlRuntimeBridgeFactory;

export function setDashboardUrlRuntimeBridgeFactoryForTest(
factory: () => DashboardUrlRuntimeBridge,
): void {
runtimeBridgeFactory = factory;
}

export function resetDashboardUrlRuntimeBridgeFactoryForTest(): void {
runtimeBridgeFactory = defaultDashboardUrlRuntimeBridgeFactory;
}

function getRuntimeBridge(): DashboardUrlRuntimeBridge {
return runtimeBridgeFactory();
}
Expand All @@ -57,7 +63,8 @@ export default class DashboardUrlCliCommand extends NemoClawCommand {
static id = "sandbox:dashboard-url";
static strict = true;
static summary = "Print the dashboard URL";
static description = "Print the browser-facing dashboard URL for a running sandbox.";
static description =
"Print the browser-facing dashboard URL and connection guidance for a running sandbox.";
static usage = ["<name> [--quiet|-q]"];
static examples = [
"<%= config.bin %> sandbox dashboard-url alpha",
Expand Down Expand Up @@ -93,6 +100,7 @@ export default class DashboardUrlCliCommand extends NemoClawCommand {
fetchToken: runtime.fetchGatewayAuthTokenFromSandbox,
getSandbox: runtime.getSandbox,
getAccessUrl: runtime.getAccessUrl,
cliName: this.config.bin,
},
);
this.setExitCode(0);
Expand Down
40 changes: 36 additions & 4 deletions src/lib/dashboard-url-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,49 @@ describe("dashboard-url command helpers", () => {
expect(sinks.out).toEqual(["http://172.22.1.1:19000/#token=secret-token"]);
});

it("prints a human label and warning outside quiet mode", () => {
it("prints the dashboard URL, connection guidance, and warning outside quiet mode (#7473)", () => {
const sinks = makeSinks();
const events: string[] = [];
runDashboardUrlCommand(
"alpha",
{ quiet: false },
{
fetchToken: () => "secret-token",
getSandbox: () => ({ agent: null, dashboardPort: 18789 }),
env: {},
log: sinks.log,
error: sinks.error,
log: (message) => {
events.push(`stdout:${message}`);
sinks.log(message);
},
error: (message) => {
events.push(`stderr:${message}`);
sinks.error(message);
},
},
);

expect(sinks.out).toEqual([" Dashboard URL:", " http://127.0.0.1:18789/#token=secret-token"]);
expect(sinks.out).toEqual([
" Dashboard URL:",
" http://127.0.0.1:18789/#token=secret-token",
"",
" Terminal:",
" nemoclaw alpha connect",
" then run: openclaw tui",
"",
" Manage later",
"",
" Status: nemoclaw alpha status",
" Logs: nemoclaw alpha logs --follow",
" Model: nemoclaw inference set --model <model> --provider <provider> --sandbox alpha",
" Policies: nemoclaw alpha policy add",
" Credentials: nemoclaw credentials reset <KEY> && nemoclaw onboard",
]);
expect(sinks.err.join("\n")).toContain("Treat this URL like a password");
expect(events.slice(0, 3)).toEqual([
"stdout: Dashboard URL:",
"stdout: http://127.0.0.1:18789/#token=secret-token",
"stderr:Treat this URL like a password -- do not log, share, or commit it.",
]);
});

it("appends an SSH port-forward hint when run over SSH (#5925)", () => {
Expand Down Expand Up @@ -115,6 +142,7 @@ describe("dashboard-url command helpers", () => {
fetchToken,
getSandbox: () => ({ agent: "hermes", dashboardPort: 18790 }),
getAgentDashboardAuth: () => "session",
cliName: "nemohermes",
env: { SSH_CONNECTION: "10.0.0.9 51000 10.6.76.40 22", USER: "spark" },
log: sinks.log,
error: sinks.error,
Expand All @@ -126,6 +154,10 @@ describe("dashboard-url command helpers", () => {
expect(sinks.out).toContain(" http://127.0.0.1:18790/");
expect(sinks.out).toContain(" Remote access (SSH session detected):");
expect(sinks.out).toContain(" ssh -L 18790:127.0.0.1:18790 spark@<host>");
expect(sinks.out).toContain(" nemohermes hermes connect");
expect(sinks.out).toContain(" Manage later");
expect(sinks.out.join("\n")).not.toContain("openclaw tui");
expect(sinks.out.join("\n")).not.toContain("#token=");
});

it("omits the SSH port-forward hint outside an SSH session", () => {
Expand Down
36 changes: 32 additions & 4 deletions src/lib/dashboard-url-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

/**
* `nemoclaw <name> dashboard-url` -- print the browser-facing dashboard URL.
* OpenClaw sandboxes still receive an authenticated token fragment, while
* session-auth agent dashboards can return the plain URL.
* `nemoclaw <name> dashboard-url` prints the browser-facing URL and connection
* guidance. Quiet mode prints only the URL. OpenClaw URLs include an
* authenticated token fragment, while session-auth dashboards return a plain URL.
*/

import { DASHBOARD_PORT } from "./core/ports";
Expand All @@ -30,6 +30,8 @@ export interface DashboardUrlCommandDeps {
log?: (message: string) => void;
/** Optional stderr sink -- defaults to console.error. */
error?: (message: string) => void;
/** CLI binary name used in follow-up commands. */
cliName?: string;
/** Environment used to detect an SSH session for the port-forward hint. */
env?: NodeJS.ProcessEnv;
}
Expand Down Expand Up @@ -65,6 +67,30 @@ function resolveDashboardPort(sandbox: Pick<SandboxEntry, "dashboardPort"> | nul
: DASHBOARD_PORT;
}

function printSandboxGuidance(
sandboxName: string,
agentName: string | null,
cliName: string,
log: (message: string) => void,
): void {
log("");
log(" Terminal:");
log(` ${cliName} ${sandboxName} connect`);
if (!agentName || agentName === "openclaw") {
log(" then run: openclaw tui");
}
log("");
log(" Manage later");
log("");
log(` Status: ${cliName} ${sandboxName} status`);
log(` Logs: ${cliName} ${sandboxName} logs --follow`);
log(
` Model: ${cliName} inference set --model <model> --provider <provider> --sandbox ${sandboxName}`,
);
log(` Policies: ${cliName} ${sandboxName} policy add`);
log(` Credentials: ${cliName} credentials reset <KEY> && ${cliName} onboard`);
}

export function buildDashboardUrl(
token: string,
port = DASHBOARD_PORT,
Expand Down Expand Up @@ -179,6 +205,7 @@ export function runDashboardUrlCommand(
log(" Dashboard URL:");
log(` ${url}`);
printSshForwardHint(port, accessUrl);
printSandboxGuidance(sandboxName, agent, deps.cliName ?? "nemoclaw", log);
return;
}

Expand Down Expand Up @@ -206,6 +233,7 @@ export function runDashboardUrlCommand(

log(" Dashboard URL:");
log(` ${url}`);
printSshForwardHint(port, accessUrl);
error(SECURITY_WARNING);
printSshForwardHint(port, accessUrl);
printSandboxGuidance(sandboxName, agent, deps.cliName ?? "nemoclaw", log);
}
Loading