summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-15 21:26:24 -0400
committerGitHub <[email protected]>2026-04-15 21:26:24 -0400
commitbbdbc107ae1f935f9694fc36b79c833643ee87a4 (patch)
tree9b70380262d3db4b36b02d9bc0d8aa6e441661fc /packages
parent0fb0135e514216732b982a9b302ee5e10f3c8e51 (diff)
downloadopencode-bbdbc107ae1f935f9694fc36b79c833643ee87a4.tar.gz
opencode-bbdbc107ae1f935f9694fc36b79c833643ee87a4.zip
feat: unwrap Config namespace to flat exports + barrel (#22689)
Diffstat (limited to 'packages')
-rwxr-xr-xpackages/opencode/script/schema.ts2
-rw-r--r--packages/opencode/src/acp/agent.ts2
-rw-r--r--packages/opencode/src/agent/agent.ts2
-rw-r--r--packages/opencode/src/cli/cmd/debug/config.ts2
-rw-r--r--packages/opencode/src/cli/cmd/mcp.ts2
-rw-r--r--packages/opencode/src/cli/cmd/providers.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui/plugin/runtime.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui/worker.ts2
-rw-r--r--packages/opencode/src/cli/error.ts2
-rw-r--r--packages/opencode/src/cli/network.ts2
-rw-r--r--packages/opencode/src/cli/upgrade.ts2
-rw-r--r--packages/opencode/src/command/index.ts2
-rw-r--r--packages/opencode/src/config/config.ts2063
-rw-r--r--packages/opencode/src/config/index.ts1
-rw-r--r--packages/opencode/src/config/tui-schema.ts2
-rw-r--r--packages/opencode/src/config/tui.ts2
-rw-r--r--packages/opencode/src/effect/app-runtime.ts2
-rw-r--r--packages/opencode/src/file/watcher.ts2
-rw-r--r--packages/opencode/src/format/index.ts2
-rw-r--r--packages/opencode/src/lsp/index.ts2
-rw-r--r--packages/opencode/src/mcp/index.ts2
-rw-r--r--packages/opencode/src/node.ts2
-rw-r--r--packages/opencode/src/permission/index.ts2
-rw-r--r--packages/opencode/src/plugin/index.ts2
-rw-r--r--packages/opencode/src/plugin/loader.ts2
-rw-r--r--packages/opencode/src/provider/provider.ts2
-rw-r--r--packages/opencode/src/server/instance/config.ts2
-rw-r--r--packages/opencode/src/server/instance/experimental.ts2
-rw-r--r--packages/opencode/src/server/instance/global.ts2
-rw-r--r--packages/opencode/src/server/instance/mcp.ts2
-rw-r--r--packages/opencode/src/server/instance/provider.ts2
-rw-r--r--packages/opencode/src/session/compaction.ts2
-rw-r--r--packages/opencode/src/session/instruction.ts2
-rw-r--r--packages/opencode/src/session/llm.ts2
-rw-r--r--packages/opencode/src/session/overflow.ts2
-rw-r--r--packages/opencode/src/session/processor.ts2
-rw-r--r--packages/opencode/src/share/session.ts2
-rw-r--r--packages/opencode/src/share/share-next.ts2
-rw-r--r--packages/opencode/src/skill/index.ts2
-rw-r--r--packages/opencode/src/snapshot/index.ts2
-rw-r--r--packages/opencode/src/tool/registry.ts2
-rw-r--r--packages/opencode/src/tool/task.ts2
-rw-r--r--packages/opencode/test/config/agent-color.test.ts2
-rw-r--r--packages/opencode/test/config/config.test.ts2
-rw-r--r--packages/opencode/test/config/tui.test.ts2
-rw-r--r--packages/opencode/test/file/watcher.test.ts2
-rw-r--r--packages/opencode/test/fixture/fixture.ts2
-rw-r--r--packages/opencode/test/permission-task.test.ts2
-rw-r--r--packages/opencode/test/session/compaction.test.ts2
-rw-r--r--packages/opencode/test/session/processor-effect.test.ts2
-rw-r--r--packages/opencode/test/session/prompt-effect.test.ts2
-rw-r--r--packages/opencode/test/session/snapshot-tool-race.test.ts2
-rw-r--r--packages/opencode/test/share/share-next.test.ts2
-rw-r--r--packages/opencode/test/tool/task.test.ts2
54 files changed, 1074 insertions, 1094 deletions
diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts
index 61d11ea7c..4ea68d9bb 100755
--- a/packages/opencode/script/schema.ts
+++ b/packages/opencode/script/schema.ts
@@ -1,7 +1,7 @@
#!/usr/bin/env bun
import { z } from "zod"
-import { Config } from "../src/config/config"
+import { Config } from "../src/config"
import { TuiConfig } from "../src/config/tui"
function generate(schema: z.ZodType) {
diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts
index 5cbf4ed1f..c065c64ff 100644
--- a/packages/opencode/src/acp/agent.ts
+++ b/packages/opencode/src/acp/agent.ts
@@ -43,7 +43,7 @@ import { Agent as AgentModule } from "../agent/agent"
import { AppRuntime } from "@/effect/app-runtime"
import { Installation } from "@/installation"
import { MessageV2 } from "@/session/message-v2"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { Todo } from "@/session/todo"
import { z } from "zod"
import { LoadAPIKeyError } from "ai"
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index ba38c8efe..5887ee28e 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -1,4 +1,4 @@
-import { Config } from "../config/config"
+import { Config } from "../config"
import z from "zod"
import { Provider } from "../provider/provider"
import { ModelID, ProviderID } from "../provider/schema"
diff --git a/packages/opencode/src/cli/cmd/debug/config.ts b/packages/opencode/src/cli/cmd/debug/config.ts
index 59e29c4a3..b1f1c25e9 100644
--- a/packages/opencode/src/cli/cmd/debug/config.ts
+++ b/packages/opencode/src/cli/cmd/debug/config.ts
@@ -1,5 +1,5 @@
import { EOL } from "os"
-import { Config } from "../../../config/config"
+import { Config } from "../../../config"
import { AppRuntime } from "@/effect/app-runtime"
import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts
index 3afedb356..b9e4b0421 100644
--- a/packages/opencode/src/cli/cmd/mcp.ts
+++ b/packages/opencode/src/cli/cmd/mcp.ts
@@ -7,7 +7,7 @@ import { UI } from "../ui"
import { MCP } from "../../mcp"
import { McpAuth } from "../../mcp/auth"
import { McpOAuthProvider } from "../../mcp/oauth-provider"
-import { Config } from "../../config/config"
+import { Config } from "../../config"
import { Instance } from "../../project/instance"
import { Installation } from "../../installation"
import path from "path"
diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts
index 6ab927e25..5b7f5a1a0 100644
--- a/packages/opencode/src/cli/cmd/providers.ts
+++ b/packages/opencode/src/cli/cmd/providers.ts
@@ -7,7 +7,7 @@ import { ModelsDev } from "../../provider/models"
import { map, pipe, sortBy, values } from "remeda"
import path from "path"
import os from "os"
-import { Config } from "../../config/config"
+import { Config } from "../../config"
import { Global } from "../../global"
import { Plugin } from "../../plugin"
import { Instance } from "../../project/instance"
diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
index 7f12106b2..bd7eac771 100644
--- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
+++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
@@ -13,7 +13,7 @@ import {
import path from "path"
import { fileURLToPath } from "url"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { TuiConfig } from "@/config/tui"
import { Log } from "@/util/log"
import { errorData, errorMessage } from "@/util/error"
diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts
index 4e1bdabcd..da9e3985b 100644
--- a/packages/opencode/src/cli/cmd/tui/worker.ts
+++ b/packages/opencode/src/cli/cmd/tui/worker.ts
@@ -5,7 +5,7 @@ import { Instance } from "@/project/instance"
import { InstanceBootstrap } from "@/project/bootstrap"
import { Rpc } from "@/util/rpc"
import { upgrade } from "@/cli/upgrade"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { GlobalBus } from "@/bus/global"
import { Flag } from "@/flag/flag"
import { writeHeapSnapshot } from "node:v8"
diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts
index cd67635e9..1277f5046 100644
--- a/packages/opencode/src/cli/error.ts
+++ b/packages/opencode/src/cli/error.ts
@@ -1,7 +1,7 @@
import { AccountServiceError, AccountTransportError } from "@/account"
import { ConfigMarkdown } from "@/config/markdown"
import { errorFormat } from "@/util/error"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { MCP } from "../mcp"
import { Provider } from "../provider/provider"
import { UI } from "./ui"
diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts
index cea49affa..6321c056d 100644
--- a/packages/opencode/src/cli/network.ts
+++ b/packages/opencode/src/cli/network.ts
@@ -1,5 +1,5 @@
import type { Argv, InferredOptionTypes } from "yargs"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { AppRuntime } from "@/effect/app-runtime"
const options = {
diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts
index f67b66245..2628f9673 100644
--- a/packages/opencode/src/cli/upgrade.ts
+++ b/packages/opencode/src/cli/upgrade.ts
@@ -1,5 +1,5 @@
import { Bus } from "@/bus"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { AppRuntime } from "@/effect/app-runtime"
import { Flag } from "@/flag/flag"
import { Installation } from "@/installation"
diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts
index 91a9e1b40..28fb37f27 100644
--- a/packages/opencode/src/command/index.ts
+++ b/packages/opencode/src/command/index.ts
@@ -5,7 +5,7 @@ import type { InstanceContext } from "@/project/instance"
import { SessionID, MessageID } from "@/session/schema"
import { Effect, Layer, Context } from "effect"
import z from "zod"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { MCP } from "../mcp"
import { Skill } from "../skill"
import { Log } from "../util/log"
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 6aee4e1dc..f35e8c83d 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -39,1133 +39,1113 @@ import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from
import { Npm } from "../npm"
import { InstanceRef } from "@/effect/instance-ref"
-export namespace Config {
- const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
- const PluginOptions = z.record(z.string(), z.unknown())
- export const PluginSpec = z.union([z.string(), z.tuple([z.string(), PluginOptions])])
-
- export type PluginOptions = z.infer<typeof PluginOptions>
- export type PluginSpec = z.infer<typeof PluginSpec>
- export type PluginScope = "global" | "local"
- export type PluginOrigin = {
- spec: PluginSpec
- source: string
- scope: PluginScope
- }
+const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
+const PluginOptions = z.record(z.string(), z.unknown())
+export const PluginSpec = z.union([z.string(), z.tuple([z.string(), PluginOptions])])
+
+export type PluginOptions = z.infer<typeof PluginOptions>
+export type PluginSpec = z.infer<typeof PluginSpec>
+export type PluginScope = "global" | "local"
+export type PluginOrigin = {
+ spec: PluginSpec
+ source: string
+ scope: PluginScope
+}
- const log = Log.create({ service: "config" })
-
- // Managed settings directory for enterprise deployments (highest priority, admin-controlled)
- // These settings override all user and project settings
- function systemManagedConfigDir(): string {
- switch (process.platform) {
- case "darwin":
- return "/Library/Application Support/opencode"
- case "win32":
- return path.join(process.env.ProgramData || "C:\\ProgramData", "opencode")
- default:
- return "/etc/opencode"
- }
+const log = Log.create({ service: "config" })
+
+// Managed settings directory for enterprise deployments (highest priority, admin-controlled)
+// These settings override all user and project settings
+function systemManagedConfigDir(): string {
+ switch (process.platform) {
+ case "darwin":
+ return "/Library/Application Support/opencode"
+ case "win32":
+ return path.join(process.env.ProgramData || "C:\\ProgramData", "opencode")
+ default:
+ return "/etc/opencode"
}
+}
- export function managedConfigDir() {
- return process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || systemManagedConfigDir()
- }
+export function managedConfigDir() {
+ return process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || systemManagedConfigDir()
+}
- const managedDir = managedConfigDir()
-
- const MANAGED_PLIST_DOMAIN = "ai.opencode.managed"
-
- // Keys injected by macOS/MDM into the managed plist that are not OpenCode config
- const PLIST_META = new Set([
- "PayloadDisplayName",
- "PayloadIdentifier",
- "PayloadType",
- "PayloadUUID",
- "PayloadVersion",
- "_manualProfile",
- ])
-
- /**
- * Parse raw JSON (from plutil conversion of a managed plist) into OpenCode config.
- * Strips MDM metadata keys before parsing through the config schema.
- * Pure function — no OS interaction, safe to unit test directly.
- */
- export function parseManagedPlist(json: string, source: string): Info {
- const raw = JSON.parse(json)
- for (const key of Object.keys(raw)) {
- if (PLIST_META.has(key)) delete raw[key]
- }
- return parseConfig(JSON.stringify(raw), source)
+const managedDir = managedConfigDir()
+
+const MANAGED_PLIST_DOMAIN = "ai.opencode.managed"
+
+// Keys injected by macOS/MDM into the managed plist that are not OpenCode config
+const PLIST_META = new Set([
+ "PayloadDisplayName",
+ "PayloadIdentifier",
+ "PayloadType",
+ "PayloadUUID",
+ "PayloadVersion",
+ "_manualProfile",
+])
+
+/**
+ * Parse raw JSON (from plutil conversion of a managed plist) into OpenCode config.
+ * Strips MDM metadata keys before parsing through the config schema.
+ * Pure function — no OS interaction, safe to unit test directly.
+ */
+export function parseManagedPlist(json: string, source: string): Info {
+ const raw = JSON.parse(json)
+ for (const key of Object.keys(raw)) {
+ if (PLIST_META.has(key)) delete raw[key]
}
+ return parseConfig(JSON.stringify(raw), source)
+}
- /**
- * Read macOS managed preferences deployed via .mobileconfig / MDM (Jamf, Kandji, etc).
- * MDM-installed profiles write to /Library/Managed Preferences/ which is only writable by root.
- * User-scoped plists are checked first, then machine-scoped.
- */
- async function readManagedPreferences(): Promise<Info> {
- if (process.platform !== "darwin") return {}
-
- const domain = MANAGED_PLIST_DOMAIN
- const user = os.userInfo().username
- const paths = [
- path.join("/Library/Managed Preferences", user, `${domain}.plist`),
- path.join("/Library/Managed Preferences", `${domain}.plist`),
- ]
-
- for (const plist of paths) {
- if (!existsSync(plist)) continue
- log.info("reading macOS managed preferences", { path: plist })
- const result = await Process.run(["plutil", "-convert", "json", "-o", "-", plist], { nothrow: true })
- if (result.code !== 0) {
- log.warn("failed to convert managed preferences plist", { path: plist })
- continue
- }
- return parseManagedPlist(result.stdout.toString(), `mobileconfig:${plist}`)
+/**
+ * Read macOS managed preferences deployed via .mobileconfig / MDM (Jamf, Kandji, etc).
+ * MDM-installed profiles write to /Library/Managed Preferences/ which is only writable by root.
+ * User-scoped plists are checked first, then machine-scoped.
+ */
+async function readManagedPreferences(): Promise<Info> {
+ if (process.platform !== "darwin") return {}
+
+ const domain = MANAGED_PLIST_DOMAIN
+ const user = os.userInfo().username
+ const paths = [
+ path.join("/Library/Managed Preferences", user, `${domain}.plist`),
+ path.join("/Library/Managed Preferences", `${domain}.plist`),
+ ]
+
+ for (const plist of paths) {
+ if (!existsSync(plist)) continue
+ log.info("reading macOS managed preferences", { path: plist })
+ const result = await Process.run(["plutil", "-convert", "json", "-o", "-", plist], { nothrow: true })
+ if (result.code !== 0) {
+ log.warn("failed to convert managed preferences plist", { path: plist })
+ continue
}
- return {}
+ return parseManagedPlist(result.stdout.toString(), `mobileconfig:${plist}`)
}
+ return {}
+}
- // Custom merge function that concatenates array fields instead of replacing them
- function mergeConfigConcatArrays(target: Info, source: Info): Info {
- const merged = mergeDeep(target, source)
- if (target.instructions && source.instructions) {
- merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions]))
- }
- return merged
+// Custom merge function that concatenates array fields instead of replacing them
+function mergeConfigConcatArrays(target: Info, source: Info): Info {
+ const merged = mergeDeep(target, source)
+ if (target.instructions && source.instructions) {
+ merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions]))
}
+ return merged
+}
- export type InstallInput = {
- waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise<void>
- }
+export type InstallInput = {
+ waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise<void>
+}
- type Package = {
- dependencies?: Record<string, string>
- }
+type Package = {
+ dependencies?: Record<string, string>
+}
- function rel(item: string, patterns: string[]) {
- const normalizedItem = item.replaceAll("\\", "/")
- for (const pattern of patterns) {
- const index = normalizedItem.indexOf(pattern)
- if (index === -1) continue
- return normalizedItem.slice(index + pattern.length)
- }
+function rel(item: string, patterns: string[]) {
+ const normalizedItem = item.replaceAll("\\", "/")
+ for (const pattern of patterns) {
+ const index = normalizedItem.indexOf(pattern)
+ if (index === -1) continue
+ return normalizedItem.slice(index + pattern.length)
}
+}
- function trim(file: string) {
- const ext = path.extname(file)
- return ext.length ? file.slice(0, -ext.length) : file
- }
+function trim(file: string) {
+ const ext = path.extname(file)
+ return ext.length ? file.slice(0, -ext.length) : file
+}
- async function loadCommand(dir: string) {
- const result: Record<string, Command> = {}
- for (const item of await Glob.scan("{command,commands}/**/*.md", {
- cwd: dir,
- absolute: true,
- dot: true,
- symlink: true,
- })) {
- const md = await ConfigMarkdown.parse(item).catch(async (err) => {
- const message = ConfigMarkdown.FrontmatterError.isInstance(err)
- ? err.data.message
- : `Failed to parse command ${item}`
- const { Session } = await import("@/session")
- Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
- log.error("failed to load command", { command: item, err })
- return undefined
- })
- if (!md) continue
+async function loadCommand(dir: string) {
+ const result: Record<string, Command> = {}
+ for (const item of await Glob.scan("{command,commands}/**/*.md", {
+ cwd: dir,
+ absolute: true,
+ dot: true,
+ symlink: true,
+ })) {
+ const md = await ConfigMarkdown.parse(item).catch(async (err) => {
+ const message = ConfigMarkdown.FrontmatterError.isInstance(err)
+ ? err.data.message
+ : `Failed to parse command ${item}`
+ const { Session } = await import("@/session")
+ Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
+ log.error("failed to load command", { command: item, err })
+ return undefined
+ })
+ if (!md) continue
- const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"]
- const file = rel(item, patterns) ?? path.basename(item)
- const name = trim(file)
+ const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"]
+ const file = rel(item, patterns) ?? path.basename(item)
+ const name = trim(file)
- const config = {
- name,
- ...md.data,
- template: md.content.trim(),
- }
- const parsed = Command.safeParse(config)
- if (parsed.success) {
- result[config.name] = parsed.data
- continue
- }
- throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
+ const config = {
+ name,
+ ...md.data,
+ template: md.content.trim(),
+ }
+ const parsed = Command.safeParse(config)
+ if (parsed.success) {
+ result[config.name] = parsed.data
+ continue
}
- return result
+ throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
}
+ return result
+}
- async function loadAgent(dir: string) {
- const result: Record<string, Agent> = {}
-
- for (const item of await Glob.scan("{agent,agents}/**/*.md", {
- cwd: dir,
- absolute: true,
- dot: true,
- symlink: true,
- })) {
- const md = await ConfigMarkdown.parse(item).catch(async (err) => {
- const message = ConfigMarkdown.FrontmatterError.isInstance(err)
- ? err.data.message
- : `Failed to parse agent ${item}`
- const { Session } = await import("@/session")
- Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
- log.error("failed to load agent", { agent: item, err })
- return undefined
- })
- if (!md) continue
+async function loadAgent(dir: string) {
+ const result: Record<string, Agent> = {}
+
+ for (const item of await Glob.scan("{agent,agents}/**/*.md", {
+ cwd: dir,
+ absolute: true,
+ dot: true,
+ symlink: true,
+ })) {
+ const md = await ConfigMarkdown.parse(item).catch(async (err) => {
+ const message = ConfigMarkdown.FrontmatterError.isInstance(err)
+ ? err.data.message
+ : `Failed to parse agent ${item}`
+ const { Session } = await import("@/session")
+ Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
+ log.error("failed to load agent", { agent: item, err })
+ return undefined
+ })
+ if (!md) continue
- const patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"]
- const file = rel(item, patterns) ?? path.basename(item)
- const agentName = trim(file)
+ const patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"]
+ const file = rel(item, patterns) ?? path.basename(item)
+ const agentName = trim(file)
- const config = {
- name: agentName,
- ...md.data,
- prompt: md.content.trim(),
- }
- const parsed = Agent.safeParse(config)
- if (parsed.success) {
- result[config.name] = parsed.data
- continue
- }
- throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
+ const config = {
+ name: agentName,
+ ...md.data,
+ prompt: md.content.trim(),
}
- return result
+ const parsed = Agent.safeParse(config)
+ if (parsed.success) {
+ result[config.name] = parsed.data
+ continue
+ }
+ throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
}
+ return result
+}
- async function loadMode(dir: string) {
- const result: Record<string, Agent> = {}
- for (const item of await Glob.scan("{mode,modes}/*.md", {
- cwd: dir,
- absolute: true,
- dot: true,
- symlink: true,
- })) {
- const md = await ConfigMarkdown.parse(item).catch(async (err) => {
- const message = ConfigMarkdown.FrontmatterError.isInstance(err)
- ? err.data.message
- : `Failed to parse mode ${item}`
- const { Session } = await import("@/session")
- Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
- log.error("failed to load mode", { mode: item, err })
- return undefined
- })
- if (!md) continue
+async function loadMode(dir: string) {
+ const result: Record<string, Agent> = {}
+ for (const item of await Glob.scan("{mode,modes}/*.md", {
+ cwd: dir,
+ absolute: true,
+ dot: true,
+ symlink: true,
+ })) {
+ const md = await ConfigMarkdown.parse(item).catch(async (err) => {
+ const message = ConfigMarkdown.FrontmatterError.isInstance(err)
+ ? err.data.message
+ : `Failed to parse mode ${item}`
+ const { Session } = await import("@/session")
+ Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
+ log.error("failed to load mode", { mode: item, err })
+ return undefined
+ })
+ if (!md) continue
- const config = {
- name: path.basename(item, ".md"),
- ...md.data,
- prompt: md.content.trim(),
- }
- const parsed = Agent.safeParse(config)
- if (parsed.success) {
- result[config.name] = {
- ...parsed.data,
- mode: "primary" as const,
- }
- continue
+ const config = {
+ name: path.basename(item, ".md"),
+ ...md.data,
+ prompt: md.content.trim(),
+ }
+ const parsed = Agent.safeParse(config)
+ if (parsed.success) {
+ result[config.name] = {
+ ...parsed.data,
+ mode: "primary" as const,
}
+ continue
}
- return result
}
+ return result
+}
- async function loadPlugin(dir: string) {
- const plugins: PluginSpec[] = []
-
- for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", {
- cwd: dir,
- absolute: true,
- dot: true,
- symlink: true,
- })) {
- plugins.push(pathToFileURL(item).href)
- }
- return plugins
- }
+async function loadPlugin(dir: string) {
+ const plugins: PluginSpec[] = []
- export function pluginSpecifier(plugin: PluginSpec): string {
- return Array.isArray(plugin) ? plugin[0] : plugin
+ for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", {
+ cwd: dir,
+ absolute: true,
+ dot: true,
+ symlink: true,
+ })) {
+ plugins.push(pathToFileURL(item).href)
}
+ return plugins
+}
- export function pluginOptions(plugin: PluginSpec): PluginOptions | undefined {
- return Array.isArray(plugin) ? plugin[1] : undefined
- }
+export function pluginSpecifier(plugin: PluginSpec): string {
+ return Array.isArray(plugin) ? plugin[0] : plugin
+}
- export async function resolvePluginSpec(plugin: PluginSpec, configFilepath: string): Promise<PluginSpec> {
- const spec = pluginSpecifier(plugin)
- if (!isPathPluginSpec(spec)) return plugin
+export function pluginOptions(plugin: PluginSpec): PluginOptions | undefined {
+ return Array.isArray(plugin) ? plugin[1] : undefined
+}
- const base = path.dirname(configFilepath)
- const file = (() => {
- if (spec.startsWith("file://")) return spec
- if (path.isAbsolute(spec) || /^[A-Za-z]:[\\/]/.test(spec)) return pathToFileURL(spec).href
- return pathToFileURL(path.resolve(base, spec)).href
- })()
+export async function resolvePluginSpec(plugin: PluginSpec, configFilepath: string): Promise<PluginSpec> {
+ const spec = pluginSpecifier(plugin)
+ if (!isPathPluginSpec(spec)) return plugin
- const resolved = await resolvePathPluginTarget(file).catch(() => file)
+ const base = path.dirname(configFilepath)
+ const file = (() => {
+ if (spec.startsWith("file://")) return spec
+ if (path.isAbsolute(spec) || /^[A-Za-z]:[\\/]/.test(spec)) return pathToFileURL(spec).href
+ return pathToFileURL(path.resolve(base, spec)).href
+ })()
- if (Array.isArray(plugin)) return [resolved, plugin[1]]
- return resolved
- }
+ const resolved = await resolvePathPluginTarget(file).catch(() => file)
- export function deduplicatePluginOrigins(plugins: PluginOrigin[]): PluginOrigin[] {
- const seen = new Set<string>()
- const list: PluginOrigin[] = []
+ if (Array.isArray(plugin)) return [resolved, plugin[1]]
+ return resolved
+}
- for (const plugin of plugins.toReversed()) {
- const spec = pluginSpecifier(plugin.spec)
- const name = spec.startsWith("file://") ? spec : parsePluginSpecifier(spec).pkg
- if (seen.has(name)) continue
- seen.add(name)
- list.push(plugin)
- }
+export function deduplicatePluginOrigins(plugins: PluginOrigin[]): PluginOrigin[] {
+ const seen = new Set<string>()
+ const list: PluginOrigin[] = []
- return list.toReversed()
+ for (const plugin of plugins.toReversed()) {
+ const spec = pluginSpecifier(plugin.spec)
+ const name = spec.startsWith("file://") ? spec : parsePluginSpecifier(spec).pkg
+ if (seen.has(name)) continue
+ seen.add(name)
+ list.push(plugin)
}
- export const McpLocal = z
- .object({
- type: z.literal("local").describe("Type of MCP server connection"),
- command: z.string().array().describe("Command and arguments to run the MCP server"),
- environment: z
- .record(z.string(), z.string())
- .optional()
- .describe("Environment variables to set when running the MCP server"),
- enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
- timeout: z
- .number()
- .int()
- .positive()
- .optional()
- .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
- })
- .strict()
- .meta({
- ref: "McpLocalConfig",
- })
-
- export const McpOAuth = z
- .object({
- clientId: z
- .string()
- .optional()
- .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."),
- clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"),
- scope: z.string().optional().describe("OAuth scopes to request during authorization"),
- redirectUri: z
- .string()
- .optional()
- .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."),
- })
- .strict()
- .meta({
- ref: "McpOAuthConfig",
- })
- export type McpOAuth = z.infer<typeof McpOAuth>
-
- export const McpRemote = z
- .object({
- type: z.literal("remote").describe("Type of MCP server connection"),
- url: z.string().describe("URL of the remote MCP server"),
- enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
- headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
- oauth: z
- .union([McpOAuth, z.literal(false)])
- .optional()
- .describe(
- "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.",
- ),
- timeout: z
- .number()
- .int()
- .positive()
- .optional()
- .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
- })
- .strict()
- .meta({
- ref: "McpRemoteConfig",
- })
-
- export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
- export type Mcp = z.infer<typeof Mcp>
+ return list.toReversed()
+}
- export const PermissionAction = z.enum(["ask", "allow", "deny"]).meta({
- ref: "PermissionActionConfig",
+export const McpLocal = z
+ .object({
+ type: z.literal("local").describe("Type of MCP server connection"),
+ command: z.string().array().describe("Command and arguments to run the MCP server"),
+ environment: z
+ .record(z.string(), z.string())
+ .optional()
+ .describe("Environment variables to set when running the MCP server"),
+ enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
+ timeout: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
})
- export type PermissionAction = z.infer<typeof PermissionAction>
-
- export const PermissionObject = z.record(z.string(), PermissionAction).meta({
- ref: "PermissionObjectConfig",
+ .strict()
+ .meta({
+ ref: "McpLocalConfig",
})
- export type PermissionObject = z.infer<typeof PermissionObject>
- export const PermissionRule = z.union([PermissionAction, PermissionObject]).meta({
- ref: "PermissionRuleConfig",
+export const McpOAuth = z
+ .object({
+ clientId: z
+ .string()
+ .optional()
+ .describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."),
+ clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"),
+ scope: z.string().optional().describe("OAuth scopes to request during authorization"),
+ redirectUri: z
+ .string()
+ .optional()
+ .describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."),
+ })
+ .strict()
+ .meta({
+ ref: "McpOAuthConfig",
+ })
+export type McpOAuth = z.infer<typeof McpOAuth>
+
+export const McpRemote = z
+ .object({
+ type: z.literal("remote").describe("Type of MCP server connection"),
+ url: z.string().describe("URL of the remote MCP server"),
+ enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
+ headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
+ oauth: z
+ .union([McpOAuth, z.literal(false)])
+ .optional()
+ .describe("OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection."),
+ timeout: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
+ })
+ .strict()
+ .meta({
+ ref: "McpRemoteConfig",
})
- export type PermissionRule = z.infer<typeof PermissionRule>
- // Capture original key order before zod reorders, then rebuild in original order
- const permissionPreprocess = (val: unknown) => {
- if (typeof val === "object" && val !== null && !Array.isArray(val)) {
- return { __originalKeys: Object.keys(val), ...val }
- }
- return val
- }
+export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
+export type Mcp = z.infer<typeof Mcp>
- const permissionTransform = (x: unknown): Record<string, PermissionRule> => {
- if (typeof x === "string") return { "*": x as PermissionAction }
- const obj = x as { __originalKeys?: string[] } & Record<string, unknown>
- const { __originalKeys, ...rest } = obj
- if (!__originalKeys) return rest as Record<string, PermissionRule>
- const result: Record<string, PermissionRule> = {}
- for (const key of __originalKeys) {
- if (key in rest) result[key] = rest[key] as PermissionRule
- }
- return result
+export const PermissionAction = z.enum(["ask", "allow", "deny"]).meta({
+ ref: "PermissionActionConfig",
+})
+export type PermissionAction = z.infer<typeof PermissionAction>
+
+export const PermissionObject = z.record(z.string(), PermissionAction).meta({
+ ref: "PermissionObjectConfig",
+})
+export type PermissionObject = z.infer<typeof PermissionObject>
+
+export const PermissionRule = z.union([PermissionAction, PermissionObject]).meta({
+ ref: "PermissionRuleConfig",
+})
+export type PermissionRule = z.infer<typeof PermissionRule>
+
+// Capture original key order before zod reorders, then rebuild in original order
+const permissionPreprocess = (val: unknown) => {
+ if (typeof val === "object" && val !== null && !Array.isArray(val)) {
+ return { __originalKeys: Object.keys(val), ...val }
}
+ return val
+}
- export const Permission = z
- .preprocess(
- permissionPreprocess,
- z
- .object({
- __originalKeys: z.string().array().optional(),
- read: PermissionRule.optional(),
- edit: PermissionRule.optional(),
- glob: PermissionRule.optional(),
- grep: PermissionRule.optional(),
- list: PermissionRule.optional(),
- bash: PermissionRule.optional(),
- task: PermissionRule.optional(),
- external_directory: PermissionRule.optional(),
- todowrite: PermissionAction.optional(),
- question: PermissionAction.optional(),
- webfetch: PermissionAction.optional(),
- websearch: PermissionAction.optional(),
- codesearch: PermissionAction.optional(),
- lsp: PermissionRule.optional(),
- doom_loop: PermissionAction.optional(),
- skill: PermissionRule.optional(),
- })
- .catchall(PermissionRule)
- .or(PermissionAction),
- )
- .transform(permissionTransform)
- .meta({
- ref: "PermissionConfig",
- })
- export type Permission = z.infer<typeof Permission>
+const permissionTransform = (x: unknown): Record<string, PermissionRule> => {
+ if (typeof x === "string") return { "*": x as PermissionAction }
+ const obj = x as { __originalKeys?: string[] } & Record<string, unknown>
+ const { __originalKeys, ...rest } = obj
+ if (!__originalKeys) return rest as Record<string, PermissionRule>
+ const result: Record<string, PermissionRule> = {}
+ for (const key of __originalKeys) {
+ if (key in rest) result[key] = rest[key] as PermissionRule
+ }
+ return result
+}
- export const Command = z.object({
- template: z.string(),
- description: z.string().optional(),
- agent: z.string().optional(),
- model: ModelId.optional(),
- subtask: z.boolean().optional(),
+export const Permission = z
+ .preprocess(
+ permissionPreprocess,
+ z
+ .object({
+ __originalKeys: z.string().array().optional(),
+ read: PermissionRule.optional(),
+ edit: PermissionRule.optional(),
+ glob: PermissionRule.optional(),
+ grep: PermissionRule.optional(),
+ list: PermissionRule.optional(),
+ bash: PermissionRule.optional(),
+ task: PermissionRule.optional(),
+ external_directory: PermissionRule.optional(),
+ todowrite: PermissionAction.optional(),
+ question: PermissionAction.optional(),
+ webfetch: PermissionAction.optional(),
+ websearch: PermissionAction.optional(),
+ codesearch: PermissionAction.optional(),
+ lsp: PermissionRule.optional(),
+ doom_loop: PermissionAction.optional(),
+ skill: PermissionRule.optional(),
+ })
+ .catchall(PermissionRule)
+ .or(PermissionAction),
+ )
+ .transform(permissionTransform)
+ .meta({
+ ref: "PermissionConfig",
})
- export type Command = z.infer<typeof Command>
-
- export const Skills = z.object({
- paths: z.array(z.string()).optional().describe("Additional paths to skill folders"),
- urls: z
- .array(z.string())
+export type Permission = z.infer<typeof Permission>
+
+export const Command = z.object({
+ template: z.string(),
+ description: z.string().optional(),
+ agent: z.string().optional(),
+ model: ModelId.optional(),
+ subtask: z.boolean().optional(),
+})
+export type Command = z.infer<typeof Command>
+
+export const Skills = z.object({
+ paths: z.array(z.string()).optional().describe("Additional paths to skill folders"),
+ urls: z
+ .array(z.string())
+ .optional()
+ .describe("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"),
+})
+export type Skills = z.infer<typeof Skills>
+
+export const Agent = z
+ .object({
+ model: ModelId.optional(),
+ variant: z
+ .string()
.optional()
- .describe("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"),
- })
- export type Skills = z.infer<typeof Skills>
-
- export const Agent = z
- .object({
- model: ModelId.optional(),
- variant: z
- .string()
- .optional()
- .describe("Default model variant for this agent (applies only when using the agent's configured model)."),
- temperature: z.number().optional(),
- top_p: z.number().optional(),
- prompt: z.string().optional(),
- tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"),
- disable: z.boolean().optional(),
- description: z.string().optional().describe("Description of when to use the agent"),
- mode: z.enum(["subagent", "primary", "all"]).optional(),
- hidden: z
- .boolean()
- .optional()
- .describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
- options: z.record(z.string(), z.any()).optional(),
- color: z
- .union([
- z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"),
- z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
- ])
- .optional()
- .describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"),
- steps: z
- .number()
- .int()
- .positive()
- .optional()
- .describe("Maximum number of agentic iterations before forcing text-only response"),
- maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
- permission: Permission.optional(),
- })
- .catchall(z.any())
- .transform((agent, ctx) => {
- const knownKeys = new Set([
- "name",
- "model",
- "variant",
- "prompt",
- "description",
- "temperature",
- "top_p",
- "mode",
- "hidden",
- "color",
- "steps",
- "maxSteps",
- "options",
- "permission",
- "disable",
- "tools",
+ .describe("Default model variant for this agent (applies only when using the agent's configured model)."),
+ temperature: z.number().optional(),
+ top_p: z.number().optional(),
+ prompt: z.string().optional(),
+ tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"),
+ disable: z.boolean().optional(),
+ description: z.string().optional().describe("Description of when to use the agent"),
+ mode: z.enum(["subagent", "primary", "all"]).optional(),
+ hidden: z
+ .boolean()
+ .optional()
+ .describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
+ options: z.record(z.string(), z.any()).optional(),
+ color: z
+ .union([
+ z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"),
+ z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
])
+ .optional()
+ .describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"),
+ steps: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe("Maximum number of agentic iterations before forcing text-only response"),
+ maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
+ permission: Permission.optional(),
+ })
+ .catchall(z.any())
+ .transform((agent, ctx) => {
+ const knownKeys = new Set([
+ "name",
+ "model",
+ "variant",
+ "prompt",
+ "description",
+ "temperature",
+ "top_p",
+ "mode",
+ "hidden",
+ "color",
+ "steps",
+ "maxSteps",
+ "options",
+ "permission",
+ "disable",
+ "tools",
+ ])
+
+ // Extract unknown properties into options
+ const options: Record<string, unknown> = { ...agent.options }
+ for (const [key, value] of Object.entries(agent)) {
+ if (!knownKeys.has(key)) options[key] = value
+ }
- // Extract unknown properties into options
- const options: Record<string, unknown> = { ...agent.options }
- for (const [key, value] of Object.entries(agent)) {
- if (!knownKeys.has(key)) options[key] = value
- }
-
- // Convert legacy tools config to permissions
- const permission: Permission = {}
- for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
- const action = enabled ? "allow" : "deny"
- // write, edit, patch, multiedit all map to edit permission
- if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
- permission.edit = action
- } else {
- permission[tool] = action
- }
+ // Convert legacy tools config to permissions
+ const permission: Permission = {}
+ for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
+ const action = enabled ? "allow" : "deny"
+ // write, edit, patch, multiedit all map to edit permission
+ if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
+ permission.edit = action
+ } else {
+ permission[tool] = action
}
- Object.assign(permission, agent.permission)
+ }
+ Object.assign(permission, agent.permission)
- // Convert legacy maxSteps to steps
- const steps = agent.steps ?? agent.maxSteps
+ // Convert legacy maxSteps to steps
+ const steps = agent.steps ?? agent.maxSteps
- return { ...agent, options, permission, steps } as typeof agent & {
- options?: Record<string, unknown>
- permission?: Permission
- steps?: number
- }
- })
- .meta({
- ref: "AgentConfig",
- })
- export type Agent = z.infer<typeof Agent>
-
- export const Keybinds = z
- .object({
- leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
- app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"),
- editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
- theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
- sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
- scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"),
- username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
- status_view: z.string().optional().default("<leader>s").describe("View status"),
- session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
- session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
- session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
- session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
- session_fork: z.string().optional().default("none").describe("Fork session from message"),
- session_rename: z.string().optional().default("ctrl+r").describe("Rename session"),
- session_delete: z.string().optional().default("ctrl+d").describe("Delete session"),
- stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"),
- model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"),
- model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"),
- session_share: z.string().optional().default("none").describe("Share current session"),
- session_unshare: z.string().optional().default("none").describe("Unshare current session"),
- session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
- session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
- messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"),
- messages_page_down: z
- .string()
- .optional()
- .default("pagedown,ctrl+alt+f")
- .describe("Scroll messages down by one page"),
- messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"),
- messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"),
- messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
- messages_half_page_down: z
- .string()
- .optional()
- .default("ctrl+alt+d")
- .describe("Scroll messages down by half page"),
- messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
- messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
- messages_next: z.string().optional().default("none").describe("Navigate to next message"),
- messages_previous: z.string().optional().default("none").describe("Navigate to previous message"),
- messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
- messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
- messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
- messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
- messages_toggle_conceal: z
- .string()
- .optional()
- .default("<leader>h")
- .describe("Toggle code block concealment in messages"),
- tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"),
- model_list: z.string().optional().default("<leader>m").describe("List available models"),
- model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
- model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
- model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"),
- model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
- command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
- agent_list: z.string().optional().default("<leader>a").describe("List agents"),
- agent_cycle: z.string().optional().default("tab").describe("Next agent"),
- agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
- variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
- variant_list: z.string().optional().default("none").describe("List model variants"),
- input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
- input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
- input_submit: z.string().optional().default("return").describe("Submit input"),
- input_newline: z
- .string()
- .optional()
- .default("shift+return,ctrl+return,alt+return,ctrl+j")
- .describe("Insert newline in input"),
- input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"),
- input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"),
- input_move_up: z.string().optional().default("up").describe("Move cursor up in input"),
- input_move_down: z.string().optional().default("down").describe("Move cursor down in input"),
- input_select_left: z.string().optional().default("shift+left").describe("Select left in input"),
- input_select_right: z.string().optional().default("shift+right").describe("Select right in input"),
- input_select_up: z.string().optional().default("shift+up").describe("Select up in input"),
- input_select_down: z.string().optional().default("shift+down").describe("Select down in input"),
- input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"),
- input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"),
- input_select_line_home: z
- .string()
- .optional()
- .default("ctrl+shift+a")
- .describe("Select to start of line in input"),
- input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"),
- input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"),
- input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"),
- input_select_visual_line_home: z
- .string()
- .optional()
- .default("alt+shift+a")
- .describe("Select to start of visual line in input"),
- input_select_visual_line_end: z
- .string()
- .optional()
- .default("alt+shift+e")
- .describe("Select to end of visual line in input"),
- input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"),
- input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"),
- input_select_buffer_home: z
- .string()
- .optional()
- .default("shift+home")
- .describe("Select to start of buffer in input"),
- input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"),
- input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"),
- input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"),
- input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
- input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
- input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
- input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
- input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
- input_word_forward: z
- .string()
- .optional()
- .default("alt+f,alt+right,ctrl+right")
- .describe("Move word forward in input"),
- input_word_backward: z
- .string()
- .optional()
- .default("alt+b,alt+left,ctrl+left")
- .describe("Move word backward in input"),
- input_select_word_forward: z
- .string()
- .optional()
- .default("alt+shift+f,alt+shift+right")
- .describe("Select word forward in input"),
- input_select_word_backward: z
- .string()
- .optional()
- .default("alt+shift+b,alt+shift+left")
- .describe("Select word backward in input"),
- input_delete_word_forward: z
- .string()
- .optional()
- .default("alt+d,alt+delete,ctrl+delete")
- .describe("Delete word forward in input"),
- input_delete_word_backward: z
- .string()
- .optional()
- .default("ctrl+w,ctrl+backspace,alt+backspace")
- .describe("Delete word backward in input"),
- history_previous: z.string().optional().default("up").describe("Previous history item"),
- history_next: z.string().optional().default("down").describe("Next history item"),
- session_child_first: z.string().optional().default("<leader>down").describe("Go to first child session"),
- session_child_cycle: z.string().optional().default("right").describe("Go to next child session"),
- session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"),
- session_parent: z.string().optional().default("up").describe("Go to parent session"),
- terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
- terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
- tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
- plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"),
- display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"),
- })
- .strict()
- .meta({
- ref: "KeybindsConfig",
- })
+ return { ...agent, options, permission, steps } as typeof agent & {
+ options?: Record<string, unknown>
+ permission?: Permission
+ steps?: number
+ }
+ })
+ .meta({
+ ref: "AgentConfig",
+ })
+export type Agent = z.infer<typeof Agent>
+
+export const Keybinds = z
+ .object({
+ leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
+ app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"),
+ editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
+ theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
+ sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
+ scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"),
+ username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
+ status_view: z.string().optional().default("<leader>s").describe("View status"),
+ session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
+ session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
+ session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
+ session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
+ session_fork: z.string().optional().default("none").describe("Fork session from message"),
+ session_rename: z.string().optional().default("ctrl+r").describe("Rename session"),
+ session_delete: z.string().optional().default("ctrl+d").describe("Delete session"),
+ stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"),
+ model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"),
+ model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"),
+ session_share: z.string().optional().default("none").describe("Share current session"),
+ session_unshare: z.string().optional().default("none").describe("Unshare current session"),
+ session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
+ session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
+ messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"),
+ messages_page_down: z
+ .string()
+ .optional()
+ .default("pagedown,ctrl+alt+f")
+ .describe("Scroll messages down by one page"),
+ messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"),
+ messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"),
+ messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
+ messages_half_page_down: z.string().optional().default("ctrl+alt+d").describe("Scroll messages down by half page"),
+ messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
+ messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
+ messages_next: z.string().optional().default("none").describe("Navigate to next message"),
+ messages_previous: z.string().optional().default("none").describe("Navigate to previous message"),
+ messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
+ messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
+ messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
+ messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
+ messages_toggle_conceal: z
+ .string()
+ .optional()
+ .default("<leader>h")
+ .describe("Toggle code block concealment in messages"),
+ tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"),
+ model_list: z.string().optional().default("<leader>m").describe("List available models"),
+ model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
+ model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
+ model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"),
+ model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
+ command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
+ agent_list: z.string().optional().default("<leader>a").describe("List agents"),
+ agent_cycle: z.string().optional().default("tab").describe("Next agent"),
+ agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
+ variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
+ variant_list: z.string().optional().default("none").describe("List model variants"),
+ input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
+ input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
+ input_submit: z.string().optional().default("return").describe("Submit input"),
+ input_newline: z
+ .string()
+ .optional()
+ .default("shift+return,ctrl+return,alt+return,ctrl+j")
+ .describe("Insert newline in input"),
+ input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"),
+ input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"),
+ input_move_up: z.string().optional().default("up").describe("Move cursor up in input"),
+ input_move_down: z.string().optional().default("down").describe("Move cursor down in input"),
+ input_select_left: z.string().optional().default("shift+left").describe("Select left in input"),
+ input_select_right: z.string().optional().default("shift+right").describe("Select right in input"),
+ input_select_up: z.string().optional().default("shift+up").describe("Select up in input"),
+ input_select_down: z.string().optional().default("shift+down").describe("Select down in input"),
+ input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"),
+ input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"),
+ input_select_line_home: z.string().optional().default("ctrl+shift+a").describe("Select to start of line in input"),
+ input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"),
+ input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"),
+ input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"),
+ input_select_visual_line_home: z
+ .string()
+ .optional()
+ .default("alt+shift+a")
+ .describe("Select to start of visual line in input"),
+ input_select_visual_line_end: z
+ .string()
+ .optional()
+ .default("alt+shift+e")
+ .describe("Select to end of visual line in input"),
+ input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"),
+ input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"),
+ input_select_buffer_home: z
+ .string()
+ .optional()
+ .default("shift+home")
+ .describe("Select to start of buffer in input"),
+ input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"),
+ input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"),
+ input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"),
+ input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
+ input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
+ input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
+ input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
+ input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
+ input_word_forward: z
+ .string()
+ .optional()
+ .default("alt+f,alt+right,ctrl+right")
+ .describe("Move word forward in input"),
+ input_word_backward: z
+ .string()
+ .optional()
+ .default("alt+b,alt+left,ctrl+left")
+ .describe("Move word backward in input"),
+ input_select_word_forward: z
+ .string()
+ .optional()
+ .default("alt+shift+f,alt+shift+right")
+ .describe("Select word forward in input"),
+ input_select_word_backward: z
+ .string()
+ .optional()
+ .default("alt+shift+b,alt+shift+left")
+ .describe("Select word backward in input"),
+ input_delete_word_forward: z
+ .string()
+ .optional()
+ .default("alt+d,alt+delete,ctrl+delete")
+ .describe("Delete word forward in input"),
+ input_delete_word_backward: z
+ .string()
+ .optional()
+ .default("ctrl+w,ctrl+backspace,alt+backspace")
+ .describe("Delete word backward in input"),
+ history_previous: z.string().optional().default("up").describe("Previous history item"),
+ history_next: z.string().optional().default("down").describe("Next history item"),
+ session_child_first: z.string().optional().default("<leader>down").describe("Go to first child session"),
+ session_child_cycle: z.string().optional().default("right").describe("Go to next child session"),
+ session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"),
+ session_parent: z.string().optional().default("up").describe("Go to parent session"),
+ terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
+ terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
+ tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
+ plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"),
+ display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"),
+ })
+ .strict()
+ .meta({
+ ref: "KeybindsConfig",
+ })
- 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"),
- mdnsDomain: z.string().optional().describe("Custom domain name for mDNS service (default: opencode.local)"),
- cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"),
- })
- .strict()
- .meta({
- ref: "ServerConfig",
- })
+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"),
+ mdnsDomain: z.string().optional().describe("Custom domain name for mDNS service (default: opencode.local)"),
+ cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"),
+ })
+ .strict()
+ .meta({
+ ref: "ServerConfig",
+ })
- export const Layout = z.enum(["auto", "stretch"]).meta({
- ref: "LayoutConfig",
+export const Layout = z.enum(["auto", "stretch"]).meta({
+ ref: "LayoutConfig",
+})
+export type Layout = z.infer<typeof Layout>
+
+export const Model = z
+ .object({
+ id: z.string(),
+ name: z.string(),
+ family: z.string().optional(),
+ release_date: z.string(),
+ attachment: z.boolean(),
+ reasoning: z.boolean(),
+ temperature: z.boolean(),
+ tool_call: z.boolean(),
+ interleaved: z
+ .union([
+ z.literal(true),
+ z
+ .object({
+ field: z.enum(["reasoning_content", "reasoning_details"]),
+ })
+ .strict(),
+ ])
+ .optional(),
+ cost: z
+ .object({
+ input: z.number(),
+ output: z.number(),
+ cache_read: z.number().optional(),
+ cache_write: z.number().optional(),
+ context_over_200k: z
+ .object({
+ input: z.number(),
+ output: z.number(),
+ cache_read: z.number().optional(),
+ cache_write: z.number().optional(),
+ })
+ .optional(),
+ })
+ .optional(),
+ limit: z.object({
+ context: z.number(),
+ input: z.number().optional(),
+ output: z.number(),
+ }),
+ modalities: z
+ .object({
+ input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+ output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
+ })
+ .optional(),
+ experimental: z.boolean().optional(),
+ status: z.enum(["alpha", "beta", "deprecated"]).optional(),
+ provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
+ options: z.record(z.string(), z.any()),
+ headers: z.record(z.string(), z.string()).optional(),
+ variants: z
+ .record(
+ z.string(),
+ z
+ .object({
+ disabled: z.boolean().optional().describe("Disable this variant for the model"),
+ })
+ .catchall(z.any()),
+ )
+ .optional()
+ .describe("Variant-specific configuration"),
+ })
+ .partial()
+
+export const Provider = z
+ .object({
+ api: z.string().optional(),
+ name: z.string(),
+ env: z.array(z.string()),
+ id: z.string(),
+ npm: z.string().optional(),
+ whitelist: z.array(z.string()).optional(),
+ blacklist: z.array(z.string()).optional(),
+ options: z
+ .object({
+ apiKey: z.string().optional(),
+ baseURL: z.string().optional(),
+ enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
+ setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"),
+ timeout: z
+ .union([
+ z
+ .number()
+ .int()
+ .positive()
+ .describe(
+ "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+ ),
+ z.literal(false).describe("Disable timeout for this provider entirely."),
+ ])
+ .optional()
+ .describe(
+ "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+ ),
+ chunkTimeout: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe(
+ "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
+ ),
+ })
+ .catchall(z.any())
+ .optional(),
+ models: z.record(z.string(), Model).optional(),
+ })
+ .partial()
+ .strict()
+ .meta({
+ ref: "ProviderConfig",
})
- export type Layout = z.infer<typeof Layout>
-
- export const Model = z
- .object({
- id: z.string(),
- name: z.string(),
- family: z.string().optional(),
- release_date: z.string(),
- attachment: z.boolean(),
- reasoning: z.boolean(),
- temperature: z.boolean(),
- tool_call: z.boolean(),
- interleaved: z
- .union([
- z.literal(true),
+
+export type Provider = z.infer<typeof Provider>
+
+export const Info = z
+ .object({
+ $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
+ logLevel: Log.Level.optional().describe("Log level"),
+ server: Server.optional().describe("Server configuration for opencode serve and web commands"),
+ command: z
+ .record(z.string(), Command)
+ .optional()
+ .describe("Command configuration, see https://opencode.ai/docs/commands"),
+ skills: Skills.optional().describe("Additional skill folder paths"),
+ watcher: z
+ .object({
+ ignore: z.array(z.string()).optional(),
+ })
+ .optional(),
+ snapshot: z
+ .boolean()
+ .optional()
+ .describe(
+ "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
+ ),
+ plugin: PluginSpec.array().optional(),
+ share: z
+ .enum(["manual", "auto", "disabled"])
+ .optional()
+ .describe(
+ "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
+ ),
+ autoshare: z
+ .boolean()
+ .optional()
+ .describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
+ autoupdate: z
+ .union([z.boolean(), z.literal("notify")])
+ .optional()
+ .describe(
+ "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
+ ),
+ disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
+ enabled_providers: z
+ .array(z.string())
+ .optional()
+ .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
+ model: ModelId.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
+ small_model: ModelId.describe(
+ "Small model to use for tasks like title generation in the format of provider/model",
+ ).optional(),
+ default_agent: z
+ .string()
+ .optional()
+ .describe(
+ "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
+ ),
+ username: z.string().optional().describe("Custom username to display in conversations instead of system username"),
+ mode: z
+ .object({
+ build: Agent.optional(),
+ plan: Agent.optional(),
+ })
+ .catchall(Agent)
+ .optional()
+ .describe("@deprecated Use `agent` field instead."),
+ agent: z
+ .object({
+ // primary
+ plan: Agent.optional(),
+ build: Agent.optional(),
+ // subagent
+ general: Agent.optional(),
+ explore: Agent.optional(),
+ // specialized
+ title: Agent.optional(),
+ summary: Agent.optional(),
+ compaction: Agent.optional(),
+ })
+ .catchall(Agent)
+ .optional()
+ .describe("Agent configuration, see https://opencode.ai/docs/agents"),
+ provider: z.record(z.string(), Provider).optional().describe("Custom provider configurations and model overrides"),
+ mcp: z
+ .record(
+ z.string(),
+ z.union([
+ Mcp,
z
.object({
- field: z.enum(["reasoning_content", "reasoning_details"]),
+ enabled: z.boolean(),
})
.strict(),
- ])
- .optional(),
- cost: z
- .object({
- input: z.number(),
- output: z.number(),
- cache_read: z.number().optional(),
- cache_write: z.number().optional(),
- context_over_200k: z
- .object({
- input: z.number(),
- output: z.number(),
- cache_read: z.number().optional(),
- cache_write: z.number().optional(),
- })
- .optional(),
- })
- .optional(),
- limit: z.object({
- context: z.number(),
- input: z.number().optional(),
- output: z.number(),
- }),
- modalities: z
- .object({
- input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
- output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
- })
- .optional(),
- experimental: z.boolean().optional(),
- status: z.enum(["alpha", "beta", "deprecated"]).optional(),
- provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
- options: z.record(z.string(), z.any()),
- headers: z.record(z.string(), z.string()).optional(),
- variants: z
- .record(
+ ]),
+ )
+ .optional()
+ .describe("MCP (Model Context Protocol) server configurations"),
+ formatter: z
+ .union([
+ z.literal(false),
+ z.record(
z.string(),
- z
- .object({
- disabled: z.boolean().optional().describe("Disable this variant for the model"),
- })
- .catchall(z.any()),
- )
- .optional()
- .describe("Variant-specific configuration"),
- })
- .partial()
-
- export const Provider = z
- .object({
- api: z.string().optional(),
- name: z.string(),
- env: z.array(z.string()),
- id: z.string(),
- npm: z.string().optional(),
- whitelist: z.array(z.string()).optional(),
- blacklist: z.array(z.string()).optional(),
- options: z
- .object({
- apiKey: z.string().optional(),
- baseURL: z.string().optional(),
- enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
- setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"),
- timeout: z
- .union([
- z
- .number()
- .int()
- .positive()
- .describe(
- "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
- ),
- z.literal(false).describe("Disable timeout for this provider entirely."),
- ])
- .optional()
- .describe(
- "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
- ),
- chunkTimeout: z
- .number()
- .int()
- .positive()
- .optional()
- .describe(
- "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
- ),
- })
- .catchall(z.any())
- .optional(),
- models: z.record(z.string(), Model).optional(),
- })
- .partial()
- .strict()
- .meta({
- ref: "ProviderConfig",
- })
-
- export type Provider = z.infer<typeof Provider>
-
- export const Info = z
- .object({
- $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
- logLevel: Log.Level.optional().describe("Log level"),
- server: Server.optional().describe("Server configuration for opencode serve and web commands"),
- command: z
- .record(z.string(), Command)
- .optional()
- .describe("Command configuration, see https://opencode.ai/docs/commands"),
- skills: Skills.optional().describe("Additional skill folder paths"),
- watcher: z
- .object({
- ignore: z.array(z.string()).optional(),
- })
- .optional(),
- snapshot: z
- .boolean()
- .optional()
- .describe(
- "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
- ),
- plugin: PluginSpec.array().optional(),
- share: z
- .enum(["manual", "auto", "disabled"])
- .optional()
- .describe(
- "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
- ),
- autoshare: z
- .boolean()
- .optional()
- .describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
- autoupdate: z
- .union([z.boolean(), z.literal("notify")])
- .optional()
- .describe(
- "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
+ z.object({
+ disabled: z.boolean().optional(),
+ command: z.array(z.string()).optional(),
+ environment: z.record(z.string(), z.string()).optional(),
+ extensions: z.array(z.string()).optional(),
+ }),
),
- disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
- enabled_providers: z
- .array(z.string())
- .optional()
- .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
- model: ModelId.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
- small_model: ModelId.describe(
- "Small model to use for tasks like title generation in the format of provider/model",
- ).optional(),
- default_agent: z
- .string()
- .optional()
- .describe(
- "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
- ),
- username: z
- .string()
- .optional()
- .describe("Custom username to display in conversations instead of system username"),
- mode: z
- .object({
- build: Agent.optional(),
- plan: Agent.optional(),
- })
- .catchall(Agent)
- .optional()
- .describe("@deprecated Use `agent` field instead."),
- agent: z
- .object({
- // primary
- plan: Agent.optional(),
- build: Agent.optional(),
- // subagent
- general: Agent.optional(),
- explore: Agent.optional(),
- // specialized
- title: Agent.optional(),
- summary: Agent.optional(),
- compaction: Agent.optional(),
- })
- .catchall(Agent)
- .optional()
- .describe("Agent configuration, see https://opencode.ai/docs/agents"),
- provider: z
- .record(z.string(), Provider)
- .optional()
- .describe("Custom provider configurations and model overrides"),
- mcp: z
- .record(
+ ])
+ .optional(),
+ lsp: z
+ .union([
+ z.literal(false),
+ z.record(
z.string(),
z.union([
- Mcp,
- z
- .object({
- enabled: z.boolean(),
- })
- .strict(),
- ]),
- )
- .optional()
- .describe("MCP (Model Context Protocol) server configurations"),
- formatter: z
- .union([
- z.literal(false),
- z.record(
- z.string(),
z.object({
- disabled: z.boolean().optional(),
- command: z.array(z.string()).optional(),
- environment: z.record(z.string(), z.string()).optional(),
+ disabled: z.literal(true),
+ }),
+ z.object({
+ command: z.array(z.string()),
extensions: z.array(z.string()).optional(),
+ disabled: z.boolean().optional(),
+ env: z.record(z.string(), z.string()).optional(),
+ initialization: z.record(z.string(), z.any()).optional(),
}),
- ),
- ])
- .optional(),
- lsp: z
- .union([
- z.literal(false),
- z.record(
- z.string(),
- z.union([
- z.object({
- disabled: z.literal(true),
- }),
- z.object({
- command: z.array(z.string()),
- extensions: z.array(z.string()).optional(),
- disabled: z.boolean().optional(),
- env: z.record(z.string(), z.string()).optional(),
- initialization: z.record(z.string(), z.any()).optional(),
- }),
- ]),
- ),
- ])
- .optional()
- .refine(
- (data) => {
- if (!data) return true
- if (typeof data === "boolean") return true
- const serverIds = new Set(Object.values(LSPServer).map((s) => s.id))
-
- return Object.entries(data).every(([id, config]) => {
- if (config.disabled) return true
- if (serverIds.has(id)) return true
- return Boolean(config.extensions)
- })
- },
- {
- error: "For custom LSP servers, 'extensions' array is required.",
- },
+ ]),
),
- instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
- layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
- permission: Permission.optional(),
- tools: z.record(z.string(), z.boolean()).optional(),
- enterprise: z
- .object({
- url: z.string().optional().describe("Enterprise URL"),
- })
- .optional(),
- compaction: z
- .object({
- auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"),
- prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"),
- reserved: z
- .number()
- .int()
- .min(0)
- .optional()
- .describe("Token buffer for compaction. Leaves enough window to avoid overflow during compaction."),
- })
- .optional(),
- experimental: z
- .object({
- disable_paste_summary: z.boolean().optional(),
- batch_tool: z.boolean().optional().describe("Enable the batch tool"),
- openTelemetry: z
- .boolean()
- .optional()
- .describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"),
- primary_tools: z
- .array(z.string())
- .optional()
- .describe("Tools that should only be available to primary agents."),
- continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"),
- mcp_timeout: z
- .number()
- .int()
- .positive()
- .optional()
- .describe("Timeout in milliseconds for model context protocol (MCP) requests"),
- })
- .optional(),
- })
- .strict()
- .meta({
- ref: "Config",
- })
+ ])
+ .optional()
+ .refine(
+ (data) => {
+ if (!data) return true
+ if (typeof data === "boolean") return true
+ const serverIds = new Set(Object.values(LSPServer).map((s) => s.id))
+
+ return Object.entries(data).every(([id, config]) => {
+ if (config.disabled) return true
+ if (serverIds.has(id)) return true
+ return Boolean(config.extensions)
+ })
+ },
+ {
+ error: "For custom LSP servers, 'extensions' array is required.",
+ },
+ ),
+ instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
+ layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
+ permission: Permission.optional(),
+ tools: z.record(z.string(), z.boolean()).optional(),
+ enterprise: z
+ .object({
+ url: z.string().optional().describe("Enterprise URL"),
+ })
+ .optional(),
+ compaction: z
+ .object({
+ auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"),
+ prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"),
+ reserved: z
+ .number()
+ .int()
+ .min(0)
+ .optional()
+ .describe("Token buffer for compaction. Leaves enough window to avoid overflow during compaction."),
+ })
+ .optional(),
+ experimental: z
+ .object({
+ disable_paste_summary: z.boolean().optional(),
+ batch_tool: z.boolean().optional().describe("Enable the batch tool"),
+ openTelemetry: z
+ .boolean()
+ .optional()
+ .describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"),
+ primary_tools: z
+ .array(z.string())
+ .optional()
+ .describe("Tools that should only be available to primary agents."),
+ continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"),
+ mcp_timeout: z
+ .number()
+ .int()
+ .positive()
+ .optional()
+ .describe("Timeout in milliseconds for model context protocol (MCP) requests"),
+ })
+ .optional(),
+ })
+ .strict()
+ .meta({
+ ref: "Config",
+ })
- export type Info = z.output<typeof Info> & {
- plugin_origins?: PluginOrigin[]
- }
+export type Info = z.output<typeof Info> & {
+ plugin_origins?: PluginOrigin[]
+}
- type State = {
- config: Info
- directories: string[]
- deps: Fiber.Fiber<void, never>[]
- consoleState: ConsoleState
- }
+type State = {
+ config: Info
+ directories: string[]
+ deps: Fiber.Fiber<void, never>[]
+ consoleState: ConsoleState
+}
- export interface Interface {
- readonly get: () => Effect.Effect<Info>
- readonly getGlobal: () => Effect.Effect<Info>
- readonly getConsoleState: () => Effect.Effect<ConsoleState>
- readonly installDependencies: (dir: string, input?: InstallInput) => Effect.Effect<void, AppFileSystem.Error>
- readonly update: (config: Info) => Effect.Effect<void>
- readonly updateGlobal: (config: Info) => Effect.Effect<Info>
- readonly invalidate: (wait?: boolean) => Effect.Effect<void>
- readonly directories: () => Effect.Effect<string[]>
- readonly waitForDependencies: () => Effect.Effect<void>
- }
+export interface Interface {
+ readonly get: () => Effect.Effect<Info>
+ readonly getGlobal: () => Effect.Effect<Info>
+ readonly getConsoleState: () => Effect.Effect<ConsoleState>
+ readonly installDependencies: (dir: string, input?: InstallInput) => Effect.Effect<void, AppFileSystem.Error>
+ readonly update: (config: Info) => Effect.Effect<void>
+ readonly updateGlobal: (config: Info) => Effect.Effect<Info>
+ readonly invalidate: (wait?: boolean) => Effect.Effect<void>
+ readonly directories: () => Effect.Effect<string[]>
+ readonly waitForDependencies: () => Effect.Effect<void>
+}
- export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {}
+export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {}
- function globalConfigFile() {
- const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) =>
- path.join(Global.Path.config, file),
- )
- for (const file of candidates) {
- if (existsSync(file)) return file
- }
- return candidates[0]
+function globalConfigFile() {
+ const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) =>
+ path.join(Global.Path.config, file),
+ )
+ for (const file of candidates) {
+ if (existsSync(file)) return file
}
+ return candidates[0]
+}
- function patchJsonc(input: string, patch: unknown, path: string[] = []): string {
- if (!isRecord(patch)) {
- const edits = modify(input, path, patch, {
- formattingOptions: {
- insertSpaces: true,
- tabSize: 2,
- },
- })
- return applyEdits(input, edits)
- }
-
- return Object.entries(patch).reduce((result, [key, value]) => {
- if (value === undefined) return result
- return patchJsonc(result, value, [...path, key])
- }, input)
+function patchJsonc(input: string, patch: unknown, path: string[] = []): string {
+ if (!isRecord(patch)) {
+ const edits = modify(input, path, patch, {
+ formattingOptions: {
+ insertSpaces: true,
+ tabSize: 2,
+ },
+ })
+ return applyEdits(input, edits)
}
- function writable(info: Info) {
- const { plugin_origins, ...next } = info
- return next
- }
+ return Object.entries(patch).reduce((result, [key, value]) => {
+ if (value === undefined) return result
+ return patchJsonc(result, value, [...path, key])
+ }, input)
+}
- function parseConfig(text: string, filepath: string): Info {
- const errors: JsoncParseError[] = []
- const data = parseJsonc(text, errors, { allowTrailingComma: true })
- if (errors.length) {
- const lines = text.split("\n")
- const errorDetails = errors
- .map((e) => {
- const beforeOffset = text.substring(0, e.offset).split("\n")
- const line = beforeOffset.length
- const column = beforeOffset[beforeOffset.length - 1].length + 1
- const problemLine = lines[line - 1]
-
- const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
- if (!problemLine) return error
-
- return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
- })
- .join("\n")
+function writable(info: Info) {
+ const { plugin_origins, ...next } = info
+ return next
+}
- throw new JsonError({
- path: filepath,
- message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
+function parseConfig(text: string, filepath: string): Info {
+ const errors: JsoncParseError[] = []
+ const data = parseJsonc(text, errors, { allowTrailingComma: true })
+ if (errors.length) {
+ const lines = text.split("\n")
+ const errorDetails = errors
+ .map((e) => {
+ const beforeOffset = text.substring(0, e.offset).split("\n")
+ const line = beforeOffset.length
+ const column = beforeOffset[beforeOffset.length - 1].length + 1
+ const problemLine = lines[line - 1]
+
+ const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
+ if (!problemLine) return error
+
+ return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
})
- }
+ .join("\n")
- const parsed = Info.safeParse(data)
- if (parsed.success) return parsed.data
-
- throw new InvalidError({
+ throw new JsonError({
path: filepath,
- issues: parsed.error.issues,
+ message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
})
}
- export const { JsonError, InvalidError } = ConfigPaths
+ const parsed = Info.safeParse(data)
+ if (parsed.success) return parsed.data
- export const ConfigDirectoryTypoError = NamedError.create(
- "ConfigDirectoryTypoError",
- z.object({
- path: z.string(),
- dir: z.string(),
- suggestion: z.string(),
- }),
- )
+ throw new InvalidError({
+ path: filepath,
+ issues: parsed.error.issues,
+ })
+}
- export const layer: Layer.Layer<
- Service,
- never,
- AppFileSystem.Service | Auth.Service | Account.Service | Env.Service
- > = Layer.effect(
+export const { JsonError, InvalidError } = ConfigPaths
+
+export const ConfigDirectoryTypoError = NamedError.create(
+ "ConfigDirectoryTypoError",
+ z.object({
+ path: z.string(),
+ dir: z.string(),
+ suggestion: z.string(),
+ }),
+)
+
+export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Auth.Service | Account.Service | Env.Service> =
+ Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
@@ -1531,9 +1511,9 @@ export namespace Config {
}
if (result.tools) {
- const perms: Record<string, Config.PermissionAction> = {}
+ const perms: Record<string, PermissionAction> = {}
for (const [tool, enabled] of Object.entries(result.tools)) {
- const action: Config.PermissionAction = enabled ? "allow" : "deny"
+ const action: PermissionAction = enabled ? "allow" : "deny"
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
perms.edit = action
continue
@@ -1654,10 +1634,9 @@ export namespace Config {
}),
)
- export const defaultLayer = layer.pipe(
- Layer.provide(AppFileSystem.defaultLayer),
- Layer.provide(Env.defaultLayer),
- Layer.provide(Auth.defaultLayer),
- Layer.provide(Account.defaultLayer),
- )
-}
+export const defaultLayer = layer.pipe(
+ Layer.provide(AppFileSystem.defaultLayer),
+ Layer.provide(Env.defaultLayer),
+ Layer.provide(Auth.defaultLayer),
+ Layer.provide(Account.defaultLayer),
+)
diff --git a/packages/opencode/src/config/index.ts b/packages/opencode/src/config/index.ts
new file mode 100644
index 000000000..60e39c316
--- /dev/null
+++ b/packages/opencode/src/config/index.ts
@@ -0,0 +1 @@
+export * as Config from "./config"
diff --git a/packages/opencode/src/config/tui-schema.ts b/packages/opencode/src/config/tui-schema.ts
index a373b4d80..fd5cd8c88 100644
--- a/packages/opencode/src/config/tui-schema.ts
+++ b/packages/opencode/src/config/tui-schema.ts
@@ -1,5 +1,5 @@
import z from "zod"
-import { Config } from "./config"
+import { Config } from "."
const KeybindOverride = z
.object(
diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts
index e64b226c1..163bd4d7d 100644
--- a/packages/opencode/src/config/tui.ts
+++ b/packages/opencode/src/config/tui.ts
@@ -2,7 +2,7 @@ import { existsSync } from "fs"
import z from "zod"
import { mergeDeep, unique } from "remeda"
import { Context, Effect, Fiber, Layer } from "effect"
-import { Config } from "./config"
+import { Config } from "."
import { ConfigPaths } from "./paths"
import { migrateTuiConfig } from "./tui-migrate"
import { TuiInfo } from "./tui-schema"
diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts
index 257922daf..54139eb77 100644
--- a/packages/opencode/src/effect/app-runtime.ts
+++ b/packages/opencode/src/effect/app-runtime.ts
@@ -6,7 +6,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Bus } from "@/bus"
import { Auth } from "@/auth"
import { Account } from "@/account"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { Git } from "@/git"
import { Ripgrep } from "@/file/ripgrep"
import { FileTime } from "@/file/time"
diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts
index 8737045c1..74966fd47 100644
--- a/packages/opencode/src/file/watcher.ts
+++ b/packages/opencode/src/file/watcher.ts
@@ -12,7 +12,7 @@ import { Flag } from "@/flag/flag"
import { Git } from "@/git"
import { Instance } from "@/project/instance"
import { lazy } from "@/util/lazy"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { FileIgnore } from "./ignore"
import { Protected } from "./protected"
import { Log } from "../util/log"
diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts
index 1aeb2e51a..595bb7a60 100644
--- a/packages/opencode/src/format/index.ts
+++ b/packages/opencode/src/format/index.ts
@@ -5,7 +5,7 @@ import { InstanceState } from "@/effect/instance-state"
import path from "path"
import { mergeDeep } from "remeda"
import z from "zod"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { Instance } from "../project/instance"
import { Log } from "../util/log"
import * as Formatter from "./formatter"
diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts
index 0c83890e5..4daacd30b 100644
--- a/packages/opencode/src/lsp/index.ts
+++ b/packages/opencode/src/lsp/index.ts
@@ -6,7 +6,7 @@ import path from "path"
import { pathToFileURL, fileURLToPath } from "url"
import { LSPServer } from "./server"
import z from "zod"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { Instance } from "../project/instance"
import { Flag } from "@/flag/flag"
import { Process } from "../util/process"
diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
index a68c6c1d8..cbaa2c24b 100644
--- a/packages/opencode/src/mcp/index.ts
+++ b/packages/opencode/src/mcp/index.ts
@@ -9,7 +9,7 @@ import {
type Tool as MCPToolDef,
ToolListChangedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { Log } from "../util/log"
import { NamedError } from "@opencode-ai/shared/util/error"
import z from "zod/v4"
diff --git a/packages/opencode/src/node.ts b/packages/opencode/src/node.ts
index 44a9f3b43..6f020576d 100644
--- a/packages/opencode/src/node.ts
+++ b/packages/opencode/src/node.ts
@@ -1,4 +1,4 @@
-export { Config } from "./config/config"
+export { Config } from "./config"
export { Server } from "./server/server"
export { bootstrap } from "./cli/bootstrap"
export { Log } from "./util/log"
diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts
index b6a44e258..71d321080 100644
--- a/packages/opencode/src/permission/index.ts
+++ b/packages/opencode/src/permission/index.ts
@@ -1,6 +1,6 @@
import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { InstanceState } from "@/effect/instance-state"
import { ProjectID } from "@/project/schema"
import { Instance } from "@/project/instance"
diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts
index 9f618eff8..f31e0b9ff 100644
--- a/packages/opencode/src/plugin/index.ts
+++ b/packages/opencode/src/plugin/index.ts
@@ -5,7 +5,7 @@ import type {
PluginModule,
WorkspaceAdaptor as PluginWorkspaceAdaptor,
} from "@opencode-ai/plugin"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { Bus } from "../bus"
import { Log } from "../util/log"
import { createOpencodeClient } from "@opencode-ai/sdk"
diff --git a/packages/opencode/src/plugin/loader.ts b/packages/opencode/src/plugin/loader.ts
index 634fe6aad..12617f901 100644
--- a/packages/opencode/src/plugin/loader.ts
+++ b/packages/opencode/src/plugin/loader.ts
@@ -1,4 +1,4 @@
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { Installation } from "@/installation"
import {
checkPluginCompatibility,
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index c029e5c5c..1dd6027db 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -1,7 +1,7 @@
import z from "zod"
import os from "os"
import fuzzysort from "fuzzysort"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
import { NoSuchModelError, type Provider as SDK } from "ai"
import { Log } from "../util/log"
diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts
index aa770726d..11845c69c 100644
--- a/packages/opencode/src/server/instance/config.ts
+++ b/packages/opencode/src/server/instance/config.ts
@@ -1,7 +1,7 @@
import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
-import { Config } from "../../config/config"
+import { Config } from "../../config"
import { Provider } from "../../provider/provider"
import { mapValues } from "remeda"
import { errors } from "../error"
diff --git a/packages/opencode/src/server/instance/experimental.ts b/packages/opencode/src/server/instance/experimental.ts
index e8e46b2e3..6e1a47ed2 100644
--- a/packages/opencode/src/server/instance/experimental.ts
+++ b/packages/opencode/src/server/instance/experimental.ts
@@ -8,7 +8,7 @@ import { Instance } from "../../project/instance"
import { Project } from "../../project/project"
import { MCP } from "../../mcp"
import { Session } from "../../session"
-import { Config } from "../../config/config"
+import { Config } from "../../config"
import { ConsoleState } from "../../config/console-state"
import { Account, AccountID, OrgID } from "../../account"
import { AppRuntime } from "../../effect/app-runtime"
diff --git a/packages/opencode/src/server/instance/global.ts b/packages/opencode/src/server/instance/global.ts
index d462a07f7..b69f35a64 100644
--- a/packages/opencode/src/server/instance/global.ts
+++ b/packages/opencode/src/server/instance/global.ts
@@ -12,7 +12,7 @@ import { Instance } from "../../project/instance"
import { Installation } from "@/installation"
import { Log } from "../../util/log"
import { lazy } from "../../util/lazy"
-import { Config } from "../../config/config"
+import { Config } from "../../config"
import { errors } from "../error"
const log = Log.create({ service: "server" })
diff --git a/packages/opencode/src/server/instance/mcp.ts b/packages/opencode/src/server/instance/mcp.ts
index f1c8701c4..695008fc4 100644
--- a/packages/opencode/src/server/instance/mcp.ts
+++ b/packages/opencode/src/server/instance/mcp.ts
@@ -2,7 +2,7 @@ import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import { MCP } from "../../mcp"
-import { Config } from "../../config/config"
+import { Config } from "../../config"
import { AppRuntime } from "../../effect/app-runtime"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts
index 6988d56e4..b9e39d4ef 100644
--- a/packages/opencode/src/server/instance/provider.ts
+++ b/packages/opencode/src/server/instance/provider.ts
@@ -1,7 +1,7 @@
import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
-import { Config } from "../../config/config"
+import { Config } from "../../config"
import { Provider } from "../../provider/provider"
import { ModelsDev } from "../../provider/models"
import { ProviderAuth } from "../../provider/auth"
diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts
index 4978ef547..810b94974 100644
--- a/packages/opencode/src/session/compaction.ts
+++ b/packages/opencode/src/session/compaction.ts
@@ -11,7 +11,7 @@ import { Log } from "../util/log"
import { SessionProcessor } from "./processor"
import { Agent } from "@/agent/agent"
import { Plugin } from "@/plugin"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { NotFoundError } from "@/storage/db"
import { ModelID, ProviderID } from "@/provider/schema"
import { Effect, Layer, Context } from "effect"
diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts
index b4794ba5b..23dd88ff5 100644
--- a/packages/opencode/src/session/instruction.ts
+++ b/packages/opencode/src/session/instruction.ts
@@ -2,7 +2,7 @@ import os from "os"
import path from "path"
import { Effect, Layer, Context } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { InstanceState } from "@/effect/instance-state"
import { Flag } from "@/flag/flag"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts
index 05d788275..2efe4a405 100644
--- a/packages/opencode/src/session/llm.ts
+++ b/packages/opencode/src/session/llm.ts
@@ -6,7 +6,7 @@ import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, json
import { mergeDeep, pipe } from "remeda"
import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
import { ProviderTransform } from "@/provider/transform"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { Instance } from "@/project/instance"
import type { Agent } from "@/agent/agent"
import type { MessageV2 } from "./message-v2"
diff --git a/packages/opencode/src/session/overflow.ts b/packages/opencode/src/session/overflow.ts
index f0e52565d..c4c6d0927 100644
--- a/packages/opencode/src/session/overflow.ts
+++ b/packages/opencode/src/session/overflow.ts
@@ -1,4 +1,4 @@
-import type { Config } from "@/config/config"
+import type { Config } from "@/config"
import type { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import type { MessageV2 } from "./message-v2"
diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts
index b02e7cc81..d91b1427b 100644
--- a/packages/opencode/src/session/processor.ts
+++ b/packages/opencode/src/session/processor.ts
@@ -2,7 +2,7 @@ import { Cause, Deferred, Effect, Layer, Context, Scope } from "effect"
import * as Stream from "effect/Stream"
import { Agent } from "@/agent/agent"
import { Bus } from "@/bus"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { Permission } from "@/permission"
import { Plugin } from "@/plugin"
import { Snapshot } from "@/snapshot"
diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts
index 08210de8a..0a673f81c 100644
--- a/packages/opencode/src/share/session.ts
+++ b/packages/opencode/src/share/session.ts
@@ -2,7 +2,7 @@ import { Session } from "@/session"
import { SessionID } from "@/session/schema"
import { SyncEvent } from "@/sync"
import { Effect, Layer, Scope, Context } from "effect"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { Flag } from "../flag/flag"
import { ShareNext } from "./share-next"
diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts
index ad247f546..667e0720c 100644
--- a/packages/opencode/src/share/share-next.ts
+++ b/packages/opencode/src/share/share-next.ts
@@ -10,7 +10,7 @@ import { Session } from "@/session"
import { MessageV2 } from "@/session/message-v2"
import type { SessionID } from "@/session/schema"
import { Database, eq } from "@/storage/db"
-import { Config } from "@/config/config"
+import { Config } from "@/config"
import { Log } from "@/util/log"
import { SessionShareTable } from "./share.sql"
diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts
index 79b426c69..4bf5d0cfe 100644
--- a/packages/opencode/src/skill/index.ts
+++ b/packages/opencode/src/skill/index.ts
@@ -11,7 +11,7 @@ import { Flag } from "@/flag/flag"
import { Global } from "@/global"
import { Permission } from "@/permission"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { ConfigMarkdown } from "../config/markdown"
import { Glob } from "@opencode-ai/shared/util/glob"
import { Log } from "../util/log"
diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts
index 9378e309a..83963e351 100644
--- a/packages/opencode/src/snapshot/index.ts
+++ b/packages/opencode/src/snapshot/index.ts
@@ -7,7 +7,7 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Hash } from "@opencode-ai/shared/util/hash"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { Global } from "../global"
import { Log } from "../util/log"
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 6900feecc..2e9971ad7 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -13,7 +13,7 @@ import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
import { SkillTool } from "./skill"
import { Tool } from "./tool"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
import z from "zod"
import { Plugin } from "../plugin"
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index ce99ab299..bbb07caa4 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -6,7 +6,7 @@ import { SessionID, MessageID } from "../session/schema"
import { MessageV2 } from "../session/message-v2"
import { Agent } from "../agent/agent"
import type { SessionPrompt } from "../session/prompt"
-import { Config } from "../config/config"
+import { Config } from "../config"
import { Effect } from "effect"
import { Log } from "@/util/log"
diff --git a/packages/opencode/test/config/agent-color.test.ts b/packages/opencode/test/config/agent-color.test.ts
index af9565cba..d77782354 100644
--- a/packages/opencode/test/config/agent-color.test.ts
+++ b/packages/opencode/test/config/agent-color.test.ts
@@ -3,7 +3,7 @@ import { Effect } from "effect"
import path from "path"
import { provideInstance, tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
-import { Config } from "../../src/config/config"
+import { Config } from "../../src/config"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Color } from "../../src/util/color"
import { AppRuntime } from "../../src/effect/app-runtime"
diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts
index ed7e689da..88957c614 100644
--- a/packages/opencode/test/config/config.test.ts
+++ b/packages/opencode/test/config/config.test.ts
@@ -1,7 +1,7 @@
import { test, expect, describe, mock, afterEach, beforeEach, spyOn } from "bun:test"
import { Deferred, Effect, Fiber, Layer, Option } from "effect"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
-import { Config } from "../../src/config/config"
+import { Config } from "../../src/config"
import { Instance } from "../../src/project/instance"
import { Auth } from "../../src/auth"
import { AccessToken, Account, AccountID, OrgID } from "../../src/account"
diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts
index 529d88bce..4767e94b0 100644
--- a/packages/opencode/test/config/tui.test.ts
+++ b/packages/opencode/test/config/tui.test.ts
@@ -3,7 +3,7 @@ import path from "path"
import fs from "fs/promises"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
-import { Config } from "../../src/config/config"
+import { Config } from "../../src/config"
import { TuiConfig } from "../../src/config/tui"
import { Global } from "../../src/global"
import { Filesystem } from "../../src/util/filesystem"
diff --git a/packages/opencode/test/file/watcher.test.ts b/packages/opencode/test/file/watcher.test.ts
index 0c8968d94..0c2355008 100644
--- a/packages/opencode/test/file/watcher.test.ts
+++ b/packages/opencode/test/file/watcher.test.ts
@@ -5,7 +5,7 @@ import path from "path"
import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect"
import { tmpdir } from "../fixture/fixture"
import { Bus } from "../../src/bus"
-import { Config } from "../../src/config/config"
+import { Config } from "../../src/config"
import { FileWatcher } from "../../src/file/watcher"
import { Git } from "../../src/git"
import { Instance } from "../../src/project/instance"
diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts
index 797054354..fd7f5e380 100644
--- a/packages/opencode/test/fixture/fixture.ts
+++ b/packages/opencode/test/fixture/fixture.ts
@@ -6,7 +6,7 @@ import { Effect, Context } from "effect"
import type * as PlatformError from "effect/PlatformError"
import type * as Scope from "effect/Scope"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
-import type { Config } from "../../src/config/config"
+import type { Config } from "../../src/config"
import { InstanceRef } from "../../src/effect/instance-ref"
import { Instance } from "../../src/project/instance"
import { TestLLMServer } from "../lib/llm-server"
diff --git a/packages/opencode/test/permission-task.test.ts b/packages/opencode/test/permission-task.test.ts
index d415d23eb..3c53314b6 100644
--- a/packages/opencode/test/permission-task.test.ts
+++ b/packages/opencode/test/permission-task.test.ts
@@ -1,6 +1,6 @@
import { afterEach, describe, test, expect } from "bun:test"
import { Permission } from "../src/permission"
-import { Config } from "../src/config/config"
+import { Config } from "../src/config"
import { Instance } from "../src/project/instance"
import { tmpdir } from "./fixture/fixture"
import { AppRuntime } from "../src/effect/app-runtime"
diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts
index 251447762..1174cdf6a 100644
--- a/packages/opencode/test/session/compaction.test.ts
+++ b/packages/opencode/test/session/compaction.test.ts
@@ -5,7 +5,7 @@ import * as Stream from "effect/Stream"
import path from "path"
import z from "zod"
import { Bus } from "../../src/bus"
-import { Config } from "../../src/config/config"
+import { Config } from "../../src/config"
import { Agent } from "../../src/agent/agent"
import { LLM } from "../../src/session/llm"
import { SessionCompaction } from "../../src/session/compaction"
diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts
index d38451308..10945be18 100644
--- a/packages/opencode/test/session/processor-effect.test.ts
+++ b/packages/opencode/test/session/processor-effect.test.ts
@@ -5,7 +5,7 @@ import path from "path"
import type { Agent } from "../../src/agent/agent"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
-import { Config } from "../../src/config/config"
+import { Config } from "../../src/config"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import { Provider } from "../../src/provider/provider"
diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts
index 31727e3df..ec1a87e96 100644
--- a/packages/opencode/test/session/prompt-effect.test.ts
+++ b/packages/opencode/test/session/prompt-effect.test.ts
@@ -7,7 +7,7 @@ import z from "zod"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { Command } from "../../src/command"
-import { Config } from "../../src/config/config"
+import { Config } from "../../src/config"
import { FileTime } from "../../src/file/time"
import { LSP } from "../../src/lsp"
import { MCP } from "../../src/mcp"
diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts
index 80d74c756..a0ea47c89 100644
--- a/packages/opencode/test/session/snapshot-tool-race.test.ts
+++ b/packages/opencode/test/session/snapshot-tool-race.test.ts
@@ -32,7 +32,7 @@ import { NodeFileSystem } from "@effect/platform-node"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { Command } from "../../src/command"
-import { Config } from "../../src/config/config"
+import { Config } from "../../src/config"
import { FileTime } from "../../src/file/time"
import { LSP } from "../../src/lsp"
import { MCP } from "../../src/mcp"
diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts
index fd230f545..135d44db0 100644
--- a/packages/opencode/test/share/share-next.test.ts
+++ b/packages/opencode/test/share/share-next.test.ts
@@ -8,7 +8,7 @@ import { Account } from "../../src/account"
import { AccountRepo } from "../../src/account/repo"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Bus } from "../../src/bus"
-import { Config } from "../../src/config/config"
+import { Config } from "../../src/config"
import { Provider } from "../../src/provider/provider"
import { Session } from "../../src/session"
import type { SessionID } from "../../src/session/schema"
diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts
index e7a143c9a..bc90dc0f2 100644
--- a/packages/opencode/test/tool/task.test.ts
+++ b/packages/opencode/test/tool/task.test.ts
@@ -1,7 +1,7 @@
import { afterEach, describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Agent } from "../../src/agent/agent"
-import { Config } from "../../src/config/config"
+import { Config } from "../../src/config"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Instance } from "../../src/project/instance"
import { Session } from "../../src/session"