Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
36ef6cc
fix(ai): finalize Bedrock redacted reasoning at block end (#51999)
rekram1-node Sep 29, 2026
34a8938
fix(ai): bind Bedrock Claude thinking blocks by default (#51959)
rekram1-node Sep 29, 2026
293d60a
chore(desktop): bump electron to 44.4.5 (#52005)
Hona Sep 29, 2026
41d4a9c
fix(session-ui): render moved notice as a timeline divider (#52009)
Hona Sep 29, 2026
a604b7f
fix(ui): calm diff word highlights with @pierre/diffs 1.5.1 (#51236)
Hona Sep 29, 2026
14af33d
fix(ai): show provider error bodies when no message is recognized (#5…
rekram1-node Sep 29, 2026
ff72659
fix(desktop): give isolated dev service a fixed port (#52013)
Hona Sep 29, 2026
2aaa265
fix(ai): read provider error messages from common body layouts (#52014)
rekram1-node Sep 29, 2026
6712cc4
fix(core): turn off Workers AI thinking through the chat template (#5…
rekram1-node Sep 29, 2026
b30c4d0
fix(ai): enable explicit caching on more message protocol routes (#51…
opencode-agent[bot] Sep 29, 2026
d76f968
feat(core): tell users how to sign back in to OpenCode Console (#51847)
vimtor Sep 29, 2026
b29dab3
fix(core): drop SSO link from Console sign-in error (#52062)
vimtor Sep 29, 2026
7ef4a1a
feat(tui): show recently closed tabs in new session menu
kitlangton Sep 29, 2026
0f931d3
chore(core): refresh bundled models.dev snapshot
opencode-agent[bot] Sep 29, 2026
948047a
fix(tui): use disclosure icon for low activity errors (#52097)
opencode-agent[bot] Sep 29, 2026
3126808
fix(ai): defer Mistral thinking metadata until block end (#52008)
rekram1-node Sep 29, 2026
a565ea8
feat(cli): allow disabling the background service (#52107)
opencode-agent[bot] Sep 29, 2026
83fafda
feat(cli): continue run after rejecting a permission ask (#51968)
jlongster Sep 29, 2026
7db941a
docs(console): add GPT 6.1 Sol to model list
fwang Sep 29, 2026
c14a1f5
feat(core): preserve provider response body in session errors (#52136)
rekram1-node Sep 29, 2026
bc5ff1c
fix(cli): answer subagent asks in run and keep parallel rejections co…
jlongster Sep 29, 2026
89d37a0
fix(core): restrict database files to their owner (#52150)
rekram1-node Sep 29, 2026
b694d2b
fix(core): explain ChatGPT token-sharing errors (#52158)
rekram1-node Sep 29, 2026
16ef62c
feat(app): show ChatGPT usage limit dialog (#52153)
rekram1-node Sep 29, 2026
621a5b0
feat(app): show ChatGPT token-sharing guidance (#52160)
rekram1-node Sep 29, 2026
4d90907
feat(core): add separate ChatGPT token-sharing OAuth method (#52147)
rekram1-node Sep 29, 2026
135995d
test: stabilize V2 CI test suites (#52159)
opencode-agent[bot] Sep 29, 2026
4c33a25
feat(cli): add auth export and auth import commands (#52139)
thdxr Sep 29, 2026
0f83038
fix(core): align ChatGPT token sharing with updated partner guide (#5…
rekram1-node Sep 29, 2026
4deda18
fix(app): align ChatGPT usage copy and links (#52163)
rekram1-node Sep 29, 2026
c2a39ac
fix(core): stop promising a ChatGPT usage reset (#52168)
rekram1-node Sep 29, 2026
a2fc196
fix(stats): match document background to page theme (#52169)
opencode-agent[bot] Sep 29, 2026
8d8a7bc
sync release versions for v2.0.20
Sep 29, 2026
cb7efec
chore(sync): merge upstream v2 through v2.0.20
cursoragent Sep 29, 2026
82349b5
test: stub connection status in fork plugin fixtures
cursoragent Sep 29, 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
Prev Previous commit
Next Next commit
feat(cli): allow disabling the background service (anomalyco#52107)
Co-authored-by: jlongster <[email protected]>
  • Loading branch information
opencode-agent[bot] and jlongster authored Sep 29, 2026
commit a565ea8c74307e407b1a5e45689d87b5e36c005d
4 changes: 2 additions & 2 deletions packages/cli/src/commands/handlers/debug/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { OpenCode } from "@opencode/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode/client/effect/service"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"

export default Runtime.handler(
Commands.commands.debug.commands.agents,
Effect.fn("cli.debug.agents")(function* () {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const response = yield* Effect.promise(() => client.agent.list({ location: { directory: process.cwd() } }))
process.stdout.write(
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/handlers/debug/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { OpenCode } from "@opencode/client"
import { Service } from "@opencode/client/effect/service"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
import { redactConfig } from "./redact"

export default Runtime.handler(
Commands.commands.debug.commands.config,
Effect.fn("cli.debug.config")(function* () {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const entries = yield* Effect.promise(() => client.config.get({ location: { directory: process.cwd() } }))
process.stdout.write(JSON.stringify(redactConfig(entries), null, 2) + EOL)
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/handlers/mcp/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { OpenCode, type IntegrationInfo, type IntegrationOAuthMethod, type McpSe
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode/client/effect/service"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
import { selectIntegration, type IntegrationChoice } from "../../../ui/integration-picker"
import { handlePromptErrors, prompt, requireInteractive } from "../../../ui/prompt"
import { answerForm } from "../auth/form"
Expand All @@ -21,7 +21,7 @@ export default Runtime.handler(
const authenticate = Effect.fn("cli.mcp.auth.run")(function* (name?: string) {
if (!name) yield* requireInteractive("Pass an MCP server name when running without an interactive terminal")
intro("Authenticate an MCP server")
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const integrations = yield* loadIntegrations(client)
const servers = yield* request((signal) => client.mcp.list({ location }, { signal }))
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/handlers/mcp/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { OpenCode, type McpServer } from "@opencode/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode/client/effect/service"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"

export default Runtime.handler(
Commands.commands.mcp.commands.list,
Effect.fn("cli.mcp.list")(function* () {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const response = yield* Effect.promise(() => client.mcp.list({ location: { directory: process.cwd() } }))
const servers = response.data.toSorted((a, b) => a.name.localeCompare(b.name))
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/handlers/mcp/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { OpenCode } from "@opencode/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode/client/effect/service"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
import { resolveIntegration } from "./resolve"

const location = { directory: process.cwd() }

export default Runtime.handler(
Commands.commands.mcp.commands.logout,
Effect.fn("cli.mcp.logout")(function* (input) {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })

const integration = yield* resolveIntegration(client, input.name, location)
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/commands/handlers/pair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { ServiceConfig } from "../../services/service-config"
export default Runtime.handler(
Commands.commands.pair,
Effect.fn("cli.pair")(function* (input: Runtime.Input<typeof Commands.commands.pair>) {
if ((yield* ServiceConfig.read()).disabled === true)
return yield* Effect.fail(
new Error("Pairing requires the background service; run `opencode service unset disabled` first"),
)
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const urls = Option.isSome(input.url)
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/handlers/plugin/inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { OpenCode, type PluginInfo } from "@opencode/client"
import { Service } from "@opencode/client/effect/service"
import { Npm } from "@opencode/util/npm"
import { Config } from "../../../config"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"

export interface Item {
readonly runtime: "Server" | "TUI"
Expand All @@ -16,7 +16,7 @@ export interface Item {
}

export const inspect = Effect.fn("cli.plugin.inspect")(function* (selected?: string) {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const location = { directory: process.cwd() }
const listed = yield* Effect.promise(() => client.plugin.list({ location }))
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/handlers/plugin/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { OpenCode, type PluginInfo } from "@opencode/client"
import { Service } from "@opencode/client/effect/service"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
import { Config } from "../../../config"
import { Global } from "@opencode/util/global"
import { Npm } from "@opencode/util/npm"
Expand All @@ -16,7 +16,7 @@ import { discoverPluginTargets, localPluginDirectories, localSource } from "@ope
export default Runtime.handler(
Commands.commands.plugin.commands.list,
Effect.fn("cli.plugin.list")(function* (input) {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const response = yield* Effect.promise(() => client.plugin.list({ location: { directory: process.cwd() } }))
const config = yield* Config.Service
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/services/server-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type Resolved = {
readonly service?: ReturnType<typeof managedService>
}

export const resolve = Effect.fn("cli.server-connection.resolve")(function* (args: Args) {
export const resolve = Effect.fn("cli.server-connection.resolve")(function* (args: Args = {}) {
if (args.server !== undefined && args.standalone)
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
if (args.server !== undefined) {
Expand All @@ -38,7 +38,7 @@ export const resolve = Effect.fn("cli.server-connection.resolve")(function* (arg
)
return { endpoint } satisfies Resolved
}
if (args.standalone) {
if (args.standalone || (yield* ServiceConfig.read()).disabled === true) {
return { endpoint: yield* Standalone.start() } satisfies Resolved
}

Expand Down
27 changes: 25 additions & 2 deletions packages/cli/src/services/service-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { selfCommand } from "../util/process"
// registration file (by channel), which version, and how to spawn opencode.

export const Info = Schema.Struct({
disabled: Schema.optional(Schema.Boolean),
hostname: Schema.optional(Schema.String),
port: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(65_535))),
password: Schema.optional(Schema.String),
Expand All @@ -20,7 +21,7 @@ export const Info = Schema.Struct({
})
export type Info = typeof Info.Type

const keys = ["hostname", "port", "password", "cors", "env"] as const
const keys = ["disabled", "hostname", "port", "password", "cors", "env"] as const
type Key = (typeof keys)[number]

const decodeInfo = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
Expand Down Expand Up @@ -78,7 +79,15 @@ export const migrateConfig = Effect.fnUntraced(function* (legacy: string, file:
})

function configKey(key: string): Key {
if (key === "hostname" || key === "port" || key === "password" || key === "cors" || key === "env") return key
if (
key === "disabled" ||
key === "hostname" ||
key === "port" ||
key === "password" ||
key === "cors" ||
key === "env"
)
return key
throw new Error(`Unknown service config key: ${key}`)
}

Expand Down Expand Up @@ -151,6 +160,9 @@ export const get = Effect.fn("cli.service-config.get")(function* (key?: string,
const selected = configKey(key)
if (selected !== "env" && name !== undefined) throw new Error(`Usage: opencode service get ${selected}`)
switch (selected) {
case "disabled": {
return String((yield* read()).disabled ?? false)
}
case "hostname": {
return (yield* read()).hostname ?? ""
}
Expand All @@ -177,6 +189,12 @@ export const set = Effect.fn("cli.service-config.set")(function* (key: string, v
if (selected !== "env" && nestedValue !== undefined)
throw new Error(`Usage: opencode service set ${selected} <value>`)
switch (selected) {
case "disabled": {
if (value !== "true" && value !== "false") throw new Error("Disabled must be true or false")
if (value === "true") yield* Service.stop(yield* options())
yield* write({ ...(yield* read()), disabled: value === "true" })
return
}
case "hostname": {
yield* Service.stop(yield* options())
yield* write({ ...(yield* read()), hostname: value })
Expand Down Expand Up @@ -221,6 +239,11 @@ export const unset = Effect.fn("cli.service-config.unset")(function* (key: strin
const selected = configKey(key)
if (selected !== "env" && name !== undefined) throw new Error(`Usage: opencode service unset ${selected}`)
switch (selected) {
case "disabled": {
const { disabled: _disabled, ...next } = yield* read()
yield* write(next)
return
}
case "hostname": {
yield* Service.stop(yield* options())
const { hostname: _hostname, ...next } = yield* read()
Expand Down
61 changes: 60 additions & 1 deletion packages/cli/test/server-connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import os from "node:os"
import path from "node:path"
import { ServerConnection } from "../src/services/server-connection"
import { ServiceConfig } from "../src/services/service-config"
import { isolatedEnv } from "./fixture/environment"

test("resolution groups Effect-native lifecycle operations only for the managed service", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-server-resolution-"))
Expand Down Expand Up @@ -38,7 +39,7 @@ test("resolution groups Effect-native lifecycle operations only for the managed
pid: process.pid,
}),
)
const resolved = await runPromise(ServerConnection.resolve({}))
const resolved = await runPromise(ServerConnection.resolve())

expect(resolved.endpoint.url).toBe(server.url.toString())
expect(resolved.service).toBeDefined()
Expand Down Expand Up @@ -69,3 +70,61 @@ test("service options only require a matching version when requested", async ()
await fs.rm(root, { recursive: true, force: true })
}
})

test("disabled background service creates a private server per CLI call without registering a daemon", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-disabled-"))
const env = isolatedEnv(root)
const command = [process.execPath, path.join(import.meta.dir, "../src/index.ts")]
const execute = async (...args: string[]) => {
const child = Bun.spawn([...command, ...args], {
cwd: path.join(import.meta.dir, ".."),
env,
stdout: "pipe",
stderr: "pipe",
})
const [stdout, stderr, exit] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exited,
])
return { stdout, stderr, exit }
}
const run = async (...args: string[]) => {
const { stdout, stderr, exit } = await execute(...args)
expect(exit, stderr).toBe(0)
expect(stderr).not.toContain("Starting background server")
return stdout
}

try {
await run("service", "set", "disabled", "true")
expect(await run("service", "get", "disabled")).toBe("true\n")
const first = JSON.parse(await run("api", "get", "/api/info"))
const second = JSON.parse(await run("api", "get", "/api/info"))
expect(first.pid).toBeGreaterThan(0)
expect(second.pid).toBeGreaterThan(0)
expect(second.pid).not.toBe(first.pid)
expect(await run("mcp", "list")).toContain("No MCP servers configured")
const pairing = await execute("pair")
expect(pairing.exit).not.toBe(0)
expect(pairing.stderr).toContain("Pairing requires the background service")

const explicit = Bun.serve({
port: 0,
fetch() {
return Response.json({ version: OPENCODE_VERSION, pid: 12345, urls: [] })
},
})
try {
expect(JSON.parse(await run("api", "--server", explicit.url.toString(), "get", "/api/info")).pid).toBe(12345)
} finally {
await explicit.stop(true)
}
expect(await fs.readdir(path.join(root, "state", "opencode")).catch(() => [])).toEqual([])

await run("service", "unset", "disabled")
expect(await run("service", "get", "disabled")).toBe("false\n")
} finally {
await fs.rm(root, { recursive: true, force: true })
}
}, 30_000)
20 changes: 19 additions & 1 deletion packages/cli/test/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Service, type Info } from "@opencode/client/effect/service"
import { Global } from "@opencode/util/global"
import { OPENCODE_VERSION } from "../src/version"
import { expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import { Effect, FileSystem, Schema } from "effect"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
Expand All @@ -21,6 +21,24 @@ test("managed service ports are stable per installation channel", () => {
expect(ServiceConfig.defaultPort("preview-a")).not.toBe(ServiceConfig.defaultPort("preview-b"))
})

test("service disabled accepts only booleans without changing configuration on invalid input", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-disabled-config-"))
const layer = Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })
const run = <A, E>(effect: Effect.Effect<A, E, Global.Service | FileSystem.FileSystem>) =>
Effect.runPromise(effect.pipe(Effect.provide(layer), Effect.provide(NodeFileSystem.layer)))
try {
expect(await run(ServiceConfig.get("disabled"))).toBe("false")
await expect(run(ServiceConfig.set("disabled", "yes"))).rejects.toThrow("Disabled must be true or false")
expect(await run(ServiceConfig.read())).toEqual({})
await run(ServiceConfig.set("disabled", "true"))
expect(await run(ServiceConfig.read())).toEqual({ disabled: true })
await run(ServiceConfig.unset("disabled"))
expect(await run(ServiceConfig.read())).toEqual({})
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})

test("local channel stores service config with the local service filename", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-"))
try {
Expand Down
11 changes: 11 additions & 0 deletions services/www/src/docs/content/cli/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ opencode --standalone
opencode --server http://localhost:4096
```

To make private servers the default for CLI commands instead of starting the shared service, disable it:

```bash
opencode service set disabled true
opencode
```

This stops the running background service. `--server` still connects to an explicitly selected server; `opencode service start`
can still start the shared service explicitly. To return to automatic service use, run `opencode service unset disabled`.
Pairing requires the shared service and is unavailable while it is disabled.

See [Troubleshooting](/troubleshooting) for shared service diagnostics and the [API reference](/api) for server endpoints.

## Paths
Expand Down