From a6ded9b60a65f41a9a68f65d8ababa478cf51f52 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Mon, 8 Jun 2026 22:15:03 -0600 Subject: [PATCH] feat(server,daemon): show local and network URLs for 0.0.0.0 binds Extract shared serverUrls helper in cli/server-urls.ts that computes local, network, and bind URLs based on the resolved hostname and port. Include urls on the Listener type returned by server.listen(). Update serve, web, daemon, and console commands to print Local/Network lines when logging a non-loopback bind, and fall back to the existing single-line format for loopback binds. Add a changeset for the user-visible output change. --- .changeset/cli-server-hostname-url-display.md | 5 ++ packages/opencode/src/cli/cmd/serve.ts | 9 ++- packages/opencode/src/cli/cmd/web.ts | 68 ++++--------------- packages/opencode/src/cli/server-urls.ts | 33 +++++++++ .../opencode/src/kilocode/cli/cmd/console.ts | 10 ++- .../opencode/src/kilocode/cli/cmd/daemon.ts | 10 ++- .../opencode/src/kilocode/daemon/daemon.ts | 9 +++ packages/opencode/src/server/server.ts | 8 +++ 8 files changed, 95 insertions(+), 57 deletions(-) create mode 100644 .changeset/cli-server-hostname-url-display.md create mode 100644 packages/opencode/src/cli/server-urls.ts diff --git a/.changeset/cli-server-hostname-url-display.md b/.changeset/cli-server-hostname-url-display.md new file mode 100644 index 0000000000..e5ab947a34 --- /dev/null +++ b/.changeset/cli-server-hostname-url-display.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": minor +--- + +Display local and network URLs when the server binds to 0.0.0.0 \ No newline at end of file diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index e8df3443d7..1311b582f2 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,5 +1,6 @@ import { Effect } from "effect" import { Server } from "../../server/server" +import { serverUrls } from "../server-urls" import { effectCmd } from "../effect-cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" import { Flag } from "@opencode-ai/core/flag/flag" @@ -18,7 +19,13 @@ export const ServeCommand = effectCmd({ } const opts = yield* resolveNetworkOptions(args) const server = yield* Effect.promise(() => Server.listen(opts)) - console.log(`kilo server listening on http://${server.hostname}:${server.port}`) // kilocode_change + const urls = serverUrls(server.hostname, server.port) + + console.log(`kilo server listening on ${urls.bind}`) + if (urls.network) { + console.log(` Local: ${urls.local}`) + console.log(` Network: ${urls.network}`) + } // kilocode_change start - graceful signal shutdown // yield* Effect.never diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 7772c48b4e..a52f3ca4ce 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -1,34 +1,12 @@ import { Effect } from "effect" import { Server } from "../../server/server" +import { serverUrls } from "../server-urls" import { UI } from "../ui" import { effectCmd } from "../effect-cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" import { Flag } from "@opencode-ai/core/flag/flag" import { InstanceRuntime } from "../../project/instance-runtime" // kilocode_change import open from "open" -import { networkInterfaces } from "os" - -function getNetworkIPs() { - const nets = networkInterfaces() - const results: string[] = [] - - for (const name of Object.keys(nets)) { - const net = nets[name] - if (!net) continue - - for (const netInfo of net) { - // Skip internal and non-IPv4 addresses - if (netInfo.internal || netInfo.family !== "IPv4") continue - - // Skip Docker bridge networks (typically 172.x.x.x) - if (netInfo.address.startsWith("172.")) continue - - results.push(netInfo.address) - } - } - - return results -} export const WebCommand = effectCmd({ command: "web", @@ -47,39 +25,23 @@ export const WebCommand = effectCmd({ UI.println(UI.logo(" ")) UI.empty() - if (opts.hostname === "0.0.0.0") { - // Show localhost for local access - const localhostUrl = `http://localhost:${server.port}` - UI.println(UI.Style.TEXT_INFO_BOLD + " Local access: ", UI.Style.TEXT_NORMAL, localhostUrl) + const urls = serverUrls(server.hostname, server.port) - // Show network IPs for remote access - const networkIPs = getNetworkIPs() - if (networkIPs.length > 0) { - for (const ip of networkIPs) { - UI.println( - UI.Style.TEXT_INFO_BOLD + " Network access: ", - UI.Style.TEXT_NORMAL, - `http://${ip}:${server.port}`, - ) - } - } - - if (opts.mdns) { - UI.println( - UI.Style.TEXT_INFO_BOLD + " mDNS: ", - UI.Style.TEXT_NORMAL, - `${opts.mdnsDomain}:${server.port}`, - ) - } - - // Open localhost in browser - open(localhostUrl).catch(() => {}) - } else { - const displayUrl = server.url.toString() - UI.println(UI.Style.TEXT_INFO_BOLD + " Web interface: ", UI.Style.TEXT_NORMAL, displayUrl) - open(displayUrl).catch(() => {}) + UI.println(UI.Style.TEXT_INFO_BOLD + " Local: ", UI.Style.TEXT_NORMAL, urls.local) + if (urls.network) { + UI.println(UI.Style.TEXT_INFO_BOLD + " Network: ", UI.Style.TEXT_NORMAL, urls.network) } + if (opts.mdns) { + UI.println( + UI.Style.TEXT_INFO_BOLD + " mDNS: ", + UI.Style.TEXT_NORMAL, + `${opts.mdnsDomain}:${server.port}`, + ) + } + + open(urls.local).catch(() => {}) + // kilocode_change start - graceful signal shutdown yield* Effect.promise( () => diff --git a/packages/opencode/src/cli/server-urls.ts b/packages/opencode/src/cli/server-urls.ts new file mode 100644 index 0000000000..f0d1f8158c --- /dev/null +++ b/packages/opencode/src/cli/server-urls.ts @@ -0,0 +1,33 @@ +import { networkInterfaces } from "os" + +export function getNetworkIPs() { + const nets = networkInterfaces() + const results: string[] = [] + + for (const name of Object.keys(nets)) { + const net = nets[name] + if (!net) continue + for (const netInfo of net) { + if (netInfo.internal || netInfo.family !== "IPv4") continue + if (netInfo.address.startsWith("172.")) continue + results.push(netInfo.address) + } + } + + return results +} + +export function serverUrls(hostname: string, port: number) { + const local = `http://localhost:${port}` + const bindStr = `http://${hostname}:${port}` + + const isLoopback = hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" + const ips = getNetworkIPs() + const lanUrl = ips.length > 0 ? `http://${ips[0]}:${port}` : undefined + + return { + local, + network: isLoopback ? lanUrl : bindStr, + bind: bindStr, + } +} diff --git a/packages/opencode/src/kilocode/cli/cmd/console.ts b/packages/opencode/src/kilocode/cli/cmd/console.ts index c3da1a3857..8971e7454f 100644 --- a/packages/opencode/src/kilocode/cli/cmd/console.ts +++ b/packages/opencode/src/kilocode/cli/cmd/console.ts @@ -1,6 +1,7 @@ import open from "open" import { cmd } from "@/cli/cmd/cmd" import { explicitNetworkOptions, withNetworkOptions, resolveNetworkOptions } from "@/cli/network" +import { serverUrls } from "@/cli/server-urls" import { AppRuntime } from "@/effect/app-runtime" import { Daemon } from "@/kilocode/daemon/daemon" import { warnPort } from "@/kilocode/cli/port-warning" @@ -48,10 +49,15 @@ export const KiloConsoleCommand = cmd({ const state = daemon.result.state if (!state) throw new Error("Kilo daemon did not provide connection state") - const url = publicUrl(state) + const urls = serverUrls(state.hostname, state.port) + const consoleLocal = `${urls.local}/console` + const consoleNetwork = urls.network ? `${urls.network}/console` : undefined + await launch(browserUrl(state)).catch((err) => { console.warn(`Could not open browser automatically: ${err instanceof Error ? err.message : String(err)}`) }) - console.log(`Kilo Console: ${url}`) + console.log("Kilo Console:") + console.log(` Local: ${consoleLocal}`) + if (consoleNetwork) console.log(` Network: ${consoleNetwork}`) }, }) diff --git a/packages/opencode/src/kilocode/cli/cmd/daemon.ts b/packages/opencode/src/kilocode/cli/cmd/daemon.ts index 70bde2822f..51da9f7290 100644 --- a/packages/opencode/src/kilocode/cli/cmd/daemon.ts +++ b/packages/opencode/src/kilocode/cli/cmd/daemon.ts @@ -1,6 +1,7 @@ import type { Argv } from "yargs" import { cmd } from "@/cli/cmd/cmd" import { explicitNetworkOptions, withNetworkOptions, resolveNetworkOptions } from "@/cli/network" +import { serverUrls } from "@/cli/server-urls" import { AppRuntime } from "@/effect/app-runtime" import { Daemon } from "@/kilocode/daemon/daemon" import { warnPort } from "@/kilocode/cli/port-warning" @@ -19,6 +20,7 @@ function safe(input: Daemon.State | undefined) { hostname: input.hostname, port: input.port, url: input.url, + urls: input.urls, username: input.username, version: input.version, startedAt: input.startedAt, @@ -47,7 +49,13 @@ function print(input: Daemon.Status, json?: boolean) { return } console.log(`kilo daemon running`) - console.log(`url: ${input.state?.url}`) + if (input.state?.urls) { + const urls = input.state.urls + console.log(`local: ${urls.local}`) + if (urls.network) console.log(`network: ${urls.network}`) + } else { + console.log(`url: ${input.state?.url}`) + } console.log(`pid: ${input.state?.pid}`) console.log(`version: ${input.health?.version ?? input.state?.version}`) console.log(`auth: enabled`) diff --git a/packages/opencode/src/kilocode/daemon/daemon.ts b/packages/opencode/src/kilocode/daemon/daemon.ts index db20c5a102..a0fa343374 100644 --- a/packages/opencode/src/kilocode/daemon/daemon.ts +++ b/packages/opencode/src/kilocode/daemon/daemon.ts @@ -9,6 +9,7 @@ import { Flock } from "@opencode-ai/core/util/flock" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" +import { serverUrls } from "@/cli/server-urls" export namespace Daemon { const username = "kilo" @@ -30,6 +31,13 @@ export namespace Daemon { hostname: z.string(), port: z.number().int().positive(), url: z.string(), + urls: z + .object({ + local: z.string(), + network: z.string().optional(), + bind: z.string(), + }) + .optional(), username: z.string(), password: z.string(), token: z.string(), @@ -205,6 +213,7 @@ export namespace Daemon { hostname: ready.hostname, port: ready.port, url: `http://${host(ready.hostname)}:${ready.port}`, + urls: serverUrls(ready.hostname, ready.port), username, password, token, diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 81f5e55c4e..e55339c077 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -2,6 +2,7 @@ import "./init-projectors" import { NodeHttpServer } from "@effect/platform-node" import * as Log from "@opencode-ai/core/util/log" +import { serverUrls } from "../cli/server-urls" import { ConfigProvider, Context, Effect, Exit, Layer, Scope } from "effect" import { HttpRouter, HttpServer } from "effect/unstable/http" import { OpenApi } from "effect/unstable/httpapi" @@ -24,6 +25,11 @@ export type Listener = { hostname: string port: number url: URL + urls: { + local: string + network?: string + bind: string + } stop: (close?: boolean) => Promise } @@ -79,6 +85,7 @@ export async function listen(opts: ListenOptions): Promise { hostname: listener.hostname, port: listener.port, url: listener.url, + urls: listener.urls, stop: (close?: boolean) => Effect.runPromiseExit(listener.stop(close)).then(() => undefined), } } @@ -96,6 +103,7 @@ const listenEffect: (opts: ListenOptions) => Effect.Effect