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.
This commit is contained in:
Aarav Sharma
2026-06-08 22:15:03 -06:00
committed by Catriel Müller
parent c1589777ff
commit a6ded9b60a
8 changed files with 95 additions and 57 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/cli": minor
---
Display local and network URLs when the server binds to 0.0.0.0
+8 -1
View File
@@ -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
+15 -53
View File
@@ -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(
() =>
+33
View File
@@ -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,
}
}
@@ -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}`)
},
})
@@ -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`)
@@ -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,
+8
View File
@@ -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<void>
}
@@ -79,6 +85,7 @@ export async function listen(opts: ListenOptions): Promise<Listener> {
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<EffectListener, unkno
hostname: opts.hostname,
port: address.port,
url: listenerUrl,
urls: serverUrls(opts.hostname, address.port),
stop: yield* makeStop(state, unpublishMdns),
}
},