summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-26 10:29:48 -0600
committerGitHub <[email protected]>2025-12-26 10:29:48 -0600
commit26e7043718fbf5fbf08eecd04ff8ed5edd82e33e (patch)
tree0e53c9d1bf2997f776cd149cf6dd9383ac0eae66
parentdd569c927a1795804d4f3bd6753ad904c4723252 (diff)
downloadopencode-26e7043718fbf5fbf08eecd04ff8ed5edd82e33e.tar.gz
opencode-26e7043718fbf5fbf08eecd04ff8ed5edd82e33e.zip
feat(core): optional mdns service (#6192)
Co-authored-by: Github Action <[email protected]>
-rw-r--r--bun.lock11
-rw-r--r--flake.lock6
-rw-r--r--nix/hashes.json2
-rw-r--r--packages/opencode/package.json1
-rw-r--r--packages/opencode/src/cli/cmd/acp.ts30
-rw-r--r--packages/opencode/src/cli/cmd/serve.ts25
-rw-r--r--packages/opencode/src/cli/cmd/tui/spawn.ts28
-rw-r--r--packages/opencode/src/cli/cmd/tui/thread.ts22
-rw-r--r--packages/opencode/src/cli/cmd/tui/worker.ts2
-rw-r--r--packages/opencode/src/cli/cmd/web.ts31
-rw-r--r--packages/opencode/src/cli/network.ts42
-rw-r--r--packages/opencode/src/config/config.ts12
-rw-r--r--packages/opencode/src/server/mdns.ts57
-rw-r--r--packages/opencode/src/server/server.ts33
-rw-r--r--packages/web/src/content/docs/cli.mdx18
-rw-r--r--packages/web/src/content/docs/config.mdx25
-rw-r--r--packages/web/src/content/docs/server.mdx9
17 files changed, 238 insertions, 116 deletions
diff --git a/bun.lock b/bun.lock
index 796cd5661..818dee4f2 100644
--- a/bun.lock
+++ b/bun.lock
@@ -292,6 +292,7 @@
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
+ "bonjour-service": "1.3.0",
"bun-pty": "0.4.2",
"chokidar": "4.0.3",
"clipboardy": "4.0.0",
@@ -1081,6 +1082,8 @@
"@kurkle/color": ["@kurkle/[email protected]", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
+ "@leichtgewicht/ip-codec": ["@leichtgewicht/[email protected]", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
+
"@mdx-js/mdx": ["@mdx-js/[email protected]", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@mixmark-io/domino": ["@mixmark-io/[email protected]", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
@@ -2003,6 +2006,8 @@
"body-parser": ["[email protected]", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
+ "bonjour-service": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="],
+
"boolbase": ["[email protected]", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"bottleneck": ["[email protected]", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
@@ -2247,6 +2252,8 @@
"dlv": ["[email protected]", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
+ "dns-packet": ["[email protected]", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="],
+
"dom-serializer": ["[email protected]", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["[email protected]", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
@@ -3023,6 +3030,8 @@
"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+ "multicast-dns": ["[email protected]", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="],
+
"mustache": ["[email protected]", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
"mysql2": ["[email protected]", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="],
@@ -3595,6 +3604,8 @@
"three": ["[email protected]", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
+ "thunky": ["[email protected]", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="],
+
"tiny-inflate": ["[email protected]", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tiny-invariant": ["[email protected]", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
diff --git a/flake.lock b/flake.lock
index 4ff2c1d0e..8bba6eeb3 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1766532406,
- "narHash": "sha256-acLU/ag9VEoKkzOD202QASX25nG1eArXg5A0mHjKgxM=",
+ "lastModified": 1766747458,
+ "narHash": "sha256-m63jjuo/ygo8ztkCziYh5OOIbTSXUDkKbqw3Vuqu4a4=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "8142186f001295e5a3239f485c8a49bf2de2695a",
+ "rev": "c633f572eded8c4f3c75b8010129854ed404a6ce",
"type": "github"
},
"original": {
diff --git a/nix/hashes.json b/nix/hashes.json
index 66c0baaf7..4363085de 100644
--- a/nix/hashes.json
+++ b/nix/hashes.json
@@ -1,3 +1,3 @@
{
- "nodeModules": "sha256-hotsyeWJA6/dP6DvZTN1Ak2RSKcsyvXlXPI/jexBHME="
+ "nodeModules": "sha256-okbViEKf1mRSmzbJgKdB9SJ875q84Bwu8d3ChHuaQ1g="
}
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index f04f0bd87..ef2b822e8 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -81,6 +81,7 @@
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
+ "bonjour-service": "1.3.0",
"bun-pty": "0.4.2",
"chokidar": "4.0.3",
"clipboardy": "4.0.0",
diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts
index c607e5f5b..2db64e3b1 100644
--- a/packages/opencode/src/cli/cmd/acp.ts
+++ b/packages/opencode/src/cli/cmd/acp.ts
@@ -3,8 +3,10 @@ import { bootstrap } from "../bootstrap"
import { cmd } from "./cmd"
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
import { ACP } from "@/acp/agent"
+import { Config } from "@/config/config"
import { Server } from "@/server/server"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
+import { withNetworkOptions, resolveNetworkOptions } from "../network"
const log = Log.create({ service: "acp-command" })
@@ -19,29 +21,17 @@ export const AcpCommand = cmd({
command: "acp",
describe: "start ACP (Agent Client Protocol) server",
builder: (yargs) => {
- return yargs
- .option("cwd", {
- describe: "working directory",
- type: "string",
- default: process.cwd(),
- })
- .option("port", {
- type: "number",
- describe: "port to listen on",
- default: 0,
- })
- .option("hostname", {
- type: "string",
- describe: "hostname to listen on",
- default: "127.0.0.1",
- })
+ return withNetworkOptions(yargs).option("cwd", {
+ describe: "working directory",
+ type: "string",
+ default: process.cwd(),
+ })
},
handler: async (args) => {
await bootstrap(process.cwd(), async () => {
- const server = Server.listen({
- port: args.port,
- hostname: args.hostname,
- })
+ const config = await Config.get()
+ const opts = resolveNetworkOptions(args, config)
+ const server = Server.listen(opts)
const sdk = createOpencodeClient({
baseUrl: `http://${server.hostname}:${server.port}`,
diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts
index 3af3316a9..0fd7aa88f 100644
--- a/packages/opencode/src/cli/cmd/serve.ts
+++ b/packages/opencode/src/cli/cmd/serve.ts
@@ -1,29 +1,16 @@
+import { Config } from "../../config/config"
import { Server } from "../../server/server"
import { cmd } from "./cmd"
+import { withNetworkOptions, resolveNetworkOptions } from "../network"
export const ServeCommand = cmd({
command: "serve",
- builder: (yargs) =>
- yargs
- .option("port", {
- alias: ["p"],
- type: "number",
- describe: "port to listen on",
- default: 0,
- })
- .option("hostname", {
- type: "string",
- describe: "hostname to listen on",
- default: "127.0.0.1",
- }),
+ builder: (yargs) => withNetworkOptions(yargs),
describe: "starts a headless opencode server",
handler: async (args) => {
- const hostname = args.hostname
- const port = args.port
- const server = Server.listen({
- port,
- hostname,
- })
+ const config = await Config.get()
+ const opts = resolveNetworkOptions(args, config)
+ const server = Server.listen(opts)
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
await new Promise(() => {})
await server.stop()
diff --git a/packages/opencode/src/cli/cmd/tui/spawn.ts b/packages/opencode/src/cli/cmd/tui/spawn.ts
index fa6795298..7ab846428 100644
--- a/packages/opencode/src/cli/cmd/tui/spawn.ts
+++ b/packages/opencode/src/cli/cmd/tui/spawn.ts
@@ -1,33 +1,23 @@
import { cmd } from "@/cli/cmd/cmd"
+import { Config } from "@/config/config"
import { Instance } from "@/project/instance"
import path from "path"
import { Server } from "@/server/server"
import { upgrade } from "@/cli/upgrade"
+import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
export const TuiSpawnCommand = cmd({
command: "spawn [project]",
builder: (yargs) =>
- yargs
- .positional("project", {
- type: "string",
- describe: "path to start opencode in",
- })
- .option("port", {
- type: "number",
- describe: "port to listen on",
- default: 0,
- })
- .option("hostname", {
- type: "string",
- describe: "hostname to listen on",
- default: "127.0.0.1",
- }),
+ withNetworkOptions(yargs).positional("project", {
+ type: "string",
+ describe: "path to start opencode in",
+ }),
handler: async (args) => {
upgrade()
- const server = Server.listen({
- port: args.port,
- hostname: "127.0.0.1",
- })
+ const config = await Config.get()
+ const opts = resolveNetworkOptions(args, config)
+ const server = Server.listen(opts)
const bin = process.execPath
const cmd = []
let cwd = process.cwd()
diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts
index 3cf8937a9..f75e3bd65 100644
--- a/packages/opencode/src/cli/cmd/tui/thread.ts
+++ b/packages/opencode/src/cli/cmd/tui/thread.ts
@@ -6,6 +6,8 @@ import path from "path"
import { UI } from "@/cli/ui"
import { iife } from "@/util/iife"
import { Log } from "@/util/log"
+import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
+import { Config } from "@/config/config"
declare global {
const OPENCODE_WORKER_PATH: string
@@ -15,7 +17,7 @@ export const TuiThreadCommand = cmd({
command: "$0 [project]",
describe: "start opencode tui",
builder: (yargs) =>
- yargs
+ withNetworkOptions(yargs)
.positional("project", {
type: "string",
describe: "path to start opencode in",
@@ -36,23 +38,12 @@ export const TuiThreadCommand = cmd({
describe: "session id to continue",
})
.option("prompt", {
- alias: ["p"],
type: "string",
describe: "prompt to use",
})
.option("agent", {
type: "string",
describe: "agent to use",
- })
- .option("port", {
- type: "number",
- describe: "port to listen on",
- default: 0,
- })
- .option("hostname", {
- type: "string",
- describe: "hostname to listen on",
- default: "127.0.0.1",
}),
handler: async (args) => {
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
@@ -87,10 +78,9 @@ export const TuiThreadCommand = cmd({
process.on("unhandledRejection", (e) => {
Log.Default.error(e)
})
- const server = await client.call("server", {
- port: args.port,
- hostname: args.hostname,
- })
+ const config = await Config.get()
+ const networkOpts = resolveNetworkOptions(args, config)
+ const server = await client.call("server", networkOpts)
const prompt = await iife(async () => {
const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
if (!args.prompt) return piped
diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts
index 76f78f3fa..3ffc45ae8 100644
--- a/packages/opencode/src/cli/cmd/tui/worker.ts
+++ b/packages/opencode/src/cli/cmd/tui/worker.ts
@@ -30,7 +30,7 @@ process.on("uncaughtException", (e) => {
let server: Bun.Server<BunWebSocketData>
export const rpc = {
- async server(input: { port: number; hostname: string }) {
+ async server(input: { port: number; hostname: string; mdns?: boolean }) {
if (server) await server.stop(true)
try {
server = Server.listen(input)
diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts
index 3d3036b1b..adede0385 100644
--- a/packages/opencode/src/cli/cmd/web.ts
+++ b/packages/opencode/src/cli/cmd/web.ts
@@ -1,6 +1,8 @@
+import { Config } from "../../config/config"
import { Server } from "../../server/server"
import { UI } from "../ui"
import { cmd } from "./cmd"
+import { withNetworkOptions, resolveNetworkOptions } from "../network"
import open from "open"
import { networkInterfaces } from "os"
@@ -28,32 +30,17 @@ function getNetworkIPs() {
export const WebCommand = cmd({
command: "web",
- builder: (yargs) =>
- yargs
- .option("port", {
- alias: ["p"],
- type: "number",
- describe: "port to listen on",
- default: 0,
- })
- .option("hostname", {
- type: "string",
- describe: "hostname to listen on",
- default: "127.0.0.1",
- }),
+ builder: (yargs) => withNetworkOptions(yargs),
describe: "starts a headless opencode server",
handler: async (args) => {
- const hostname = args.hostname
- const port = args.port
- const server = Server.listen({
- port,
- hostname,
- })
+ const config = await Config.get()
+ const opts = resolveNetworkOptions(args, config)
+ const server = Server.listen(opts)
UI.empty()
UI.println(UI.logo(" "))
UI.empty()
- if (hostname === "0.0.0.0") {
+ 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)
@@ -70,6 +57,10 @@ export const WebCommand = cmd({
}
}
+ if (opts.mdns) {
+ UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, "opencode.local")
+ }
+
// Open localhost in browser
open(localhostUrl.toString()).catch(() => {})
} else {
diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts
new file mode 100644
index 000000000..661688572
--- /dev/null
+++ b/packages/opencode/src/cli/network.ts
@@ -0,0 +1,42 @@
+import type { Argv, InferredOptionTypes } from "yargs"
+import type { Config } from "../config/config"
+
+const options = {
+ port: {
+ type: "number" as const,
+ describe: "port to listen on",
+ default: 0,
+ },
+ hostname: {
+ type: "string" as const,
+ describe: "hostname to listen on",
+ default: "127.0.0.1",
+ },
+ mdns: {
+ type: "boolean" as const,
+ describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)",
+ default: false,
+ },
+}
+
+export type NetworkOptions = InferredOptionTypes<typeof options>
+
+export function withNetworkOptions<T>(yargs: Argv<T>) {
+ return yargs.options(options)
+}
+
+export function resolveNetworkOptions(args: NetworkOptions, config?: Config.Info) {
+ const portExplicitlySet = process.argv.includes("--port")
+ const hostnameExplicitlySet = process.argv.includes("--hostname")
+ const mdnsExplicitlySet = process.argv.includes("--mdns")
+
+ const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
+ const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
+ const hostname = hostnameExplicitlySet
+ ? args.hostname
+ : mdns && !config?.server?.hostname
+ ? "0.0.0.0"
+ : (config?.server?.hostname ?? args.hostname)
+
+ return { hostname, port, mdns }
+}
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index ba9d19730..9187ed674 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -587,6 +587,17 @@ export namespace Config {
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
})
+ export const Server = z
+ .object({
+ port: z.number().int().positive().optional().describe("Port to listen on"),
+ hostname: z.string().optional().describe("Hostname to listen on"),
+ mdns: z.boolean().optional().describe("Enable mDNS service discovery"),
+ })
+ .strict()
+ .meta({
+ ref: "ServerConfig",
+ })
+
export const Layout = z.enum(["auto", "stretch"]).meta({
ref: "LayoutConfig",
})
@@ -634,6 +645,7 @@ export namespace Config {
theme: z.string().optional().describe("Theme name to use for the interface"),
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
tui: TUI.optional().describe("TUI specific settings"),
+ server: Server.optional().describe("Server configuration for opencode serve and web commands"),
command: z
.record(z.string(), Command)
.optional()
diff --git a/packages/opencode/src/server/mdns.ts b/packages/opencode/src/server/mdns.ts
new file mode 100644
index 000000000..45e61d361
--- /dev/null
+++ b/packages/opencode/src/server/mdns.ts
@@ -0,0 +1,57 @@
+import { Log } from "@/util/log"
+import Bonjour from "bonjour-service"
+
+const log = Log.create({ service: "mdns" })
+
+export namespace MDNS {
+ let bonjour: Bonjour | undefined
+ let currentPort: number | undefined
+
+ export function publish(port: number, name = "opencode") {
+ if (currentPort === port) return
+ if (bonjour) unpublish()
+
+ try {
+ bonjour = new Bonjour()
+ const service = bonjour.publish({
+ name,
+ type: "http",
+ port,
+ txt: { path: "/" },
+ })
+
+ service.on("up", () => {
+ log.info("mDNS service published", { name, port })
+ })
+
+ service.on("error", (err) => {
+ log.error("mDNS service error", { error: err })
+ })
+
+ currentPort = port
+ } catch (err) {
+ log.error("mDNS publish failed", { error: err })
+ if (bonjour) {
+ try {
+ bonjour.destroy()
+ } catch {}
+ }
+ bonjour = undefined
+ currentPort = undefined
+ }
+ }
+
+ export function unpublish() {
+ if (bonjour) {
+ try {
+ bonjour.unpublishAll()
+ bonjour.destroy()
+ } catch (err) {
+ log.error("mDNS unpublish failed", { error: err })
+ }
+ bonjour = undefined
+ currentPort = undefined
+ log.info("mDNS service unpublished")
+ }
+ }
+}
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index c74dbbb41..65393e128 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -45,9 +45,11 @@ import { Snapshot } from "@/snapshot"
import { SessionSummary } from "@/session/summary"
import { SessionStatus } from "@/session/status"
import { upgradeWebSocket, websocket } from "hono/bun"
+import type { BunWebSocketData } from "hono/bun"
import { errors } from "./error"
import { Pty } from "@/pty"
import { Installation } from "@/installation"
+import { MDNS } from "./mdns"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false
@@ -2623,20 +2625,41 @@ export namespace Server {
return result
}
- export function listen(opts: { port: number; hostname: string }) {
+ export function listen(opts: { port: number; hostname: string; mdns?: boolean }) {
const args = {
hostname: opts.hostname,
idleTimeout: 0,
fetch: App().fetch,
websocket: websocket,
} as const
- if (opts.port === 0) {
+ const tryServe = (port: number) => {
try {
- return Bun.serve({ ...args, port: 4096 })
+ return Bun.serve({ ...args, port })
} catch {
- // port 4096 not available, fall through to use port 0
+ return undefined
}
}
- return Bun.serve({ ...args, port: opts.port })
+ const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)
+ if (!server) throw new Error(`Failed to start server on port ${opts.port}`)
+
+ const shouldPublishMDNS =
+ opts.mdns &&
+ server.port &&
+ opts.hostname !== "127.0.0.1" &&
+ opts.hostname !== "localhost" &&
+ opts.hostname !== "::1"
+ if (shouldPublishMDNS) {
+ MDNS.publish(server.port!)
+ } else if (opts.mdns) {
+ log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
+ }
+
+ const originalStop = server.stop.bind(server)
+ server.stop = async (closeActiveConnections?: boolean) => {
+ if (shouldPublishMDNS) MDNS.unpublish()
+ return originalStop(closeActiveConnections)
+ }
+
+ return server
}
}
diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx
index e4e40ac7a..4a826e5b3 100644
--- a/packages/web/src/content/docs/cli.mdx
+++ b/packages/web/src/content/docs/cli.mdx
@@ -335,10 +335,11 @@ This starts an HTTP server that provides API access to opencode functionality wi
#### Flags
-| Flag | Short | Description |
-| ------------ | ----- | --------------------- |
-| `--port` | `-p` | Port to listen on |
-| `--hostname` | | Hostname to listen on |
+| Flag | Description |
+| ------------ | --------------------- |
+| `--port` | Port to listen on |
+| `--hostname` | Hostname to listen on |
+| `--mdns` | Enable mDNS discovery |
---
@@ -428,10 +429,11 @@ This starts an HTTP server and opens a web browser to access OpenCode through a
#### Flags
-| Flag | Short | Description |
-| ------------ | ----- | --------------------- |
-| `--port` | `-p` | Port to listen on |
-| `--hostname` | | Hostname to listen on |
+| Flag | Description |
+| ------------ | --------------------- |
+| `--port` | Port to listen on |
+| `--hostname` | Hostname to listen on |
+| `--mdns` | Enable mDNS discovery |
---
diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx
index ebaff36bb..d7f803178 100644
--- a/packages/web/src/content/docs/config.mdx
+++ b/packages/web/src/content/docs/config.mdx
@@ -120,6 +120,31 @@ Available options:
---
+### Server
+
+You can configure server settings for the `opencode serve` and `opencode web` commands through the `server` option.
+
+```json title="opencode.json"
+{
+ "$schema": "https://opencode.ai/config.json",
+ "server": {
+ "port": 4096,
+ "hostname": "0.0.0.0",
+ "mdns": true
+ }
+}
+```
+
+Available options:
+
+- `port` - Port to listen on.
+- `hostname` - Hostname to listen on. When `mdns` is enabled and no hostname is set, defaults to `0.0.0.0`.
+- `mdns` - Enable mDNS service discovery. This allows other devices on the network to discover your OpenCode server.
+
+[Learn more about the server here](/docs/server).
+
+---
+
### Tools
You can manage the tools an LLM can use through the `tools` option.
diff --git a/packages/web/src/content/docs/server.mdx b/packages/web/src/content/docs/server.mdx
index 427d8f505..c63917f79 100644
--- a/packages/web/src/content/docs/server.mdx
+++ b/packages/web/src/content/docs/server.mdx
@@ -18,10 +18,11 @@ opencode serve [--port <number>] [--hostname <string>]
#### Options
-| Flag | Short | Description | Default |
-| ------------ | ----- | --------------------- | ----------- |
-| `--port` | `-p` | Port to listen on | `4096` |
-| `--hostname` | `-h` | Hostname to listen on | `127.0.0.1` |
+| Flag | Description | Default |
+| ------------ | --------------------- | ----------- |
+| `--port` | Port to listen on | `4096` |
+| `--hostname` | Hostname to listen on | `127.0.0.1` |
+| `--mdns` | Enable mDNS discovery | `false` |
---