diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts index 61d11ea7c9..4ea68d9bbb 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 5cbf4ed1f9..c065c64ffc 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 ba38c8efe3..5887ee28e3 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 59e29c4a38..b1f1c25e9c 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 3afedb356d..b9e4b04219 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 6ab927e253..5b7f5a1a0d 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 7f12106b2c..bd7eac7713 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 4e1bdabcdd..da9e3985b5 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 cd67635e9b..1277f5046c 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 cea49affa5..6321c056d0 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 f67b662455..2628f9673f 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 91a9e1b405..28fb37f272 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 6aee4e1dc8..f35e8c83df 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])]) +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 - export type PluginSpec = z.infer - export type PluginScope = "global" | "local" - export type PluginOrigin = { - spec: PluginSpec - source: string - scope: PluginScope +export type PluginOptions = z.infer +export type PluginSpec = z.infer +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" }) +export function managedConfigDir() { + return process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || systemManagedConfigDir() +} - // 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 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 { + 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}`) } + return {} +} - export function managedConfigDir() { - return process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR || systemManagedConfigDir() +// 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 +} - const managedDir = managedConfigDir() +export type InstallInput = { + waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise +} - const MANAGED_PLIST_DOMAIN = "ai.opencode.managed" +type Package = { + dependencies?: Record +} - // 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) +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) } +} - /** - * 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 { - if (process.platform !== "darwin") return {} +function trim(file: string) { + const ext = path.extname(file) + return ext.length ? file.slice(0, -ext.length) : file +} - 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}`) - } - 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 - } - - export type InstallInput = { - waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise - } - - type Package = { - dependencies?: Record - } - - 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 - } - - async function loadCommand(dir: string) { - const result: Record = {} - 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 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 }) - } - return result - } - - async function loadAgent(dir: string) { - const result: Record = {} - - 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 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 }) - } - return result - } - - async function loadMode(dir: string) { - const result: Record = {} - 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 - } - } - 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 - } - - export function pluginSpecifier(plugin: PluginSpec): string { - return Array.isArray(plugin) ? plugin[0] : plugin - } - - export function pluginOptions(plugin: PluginSpec): PluginOptions | undefined { - return Array.isArray(plugin) ? plugin[1] : undefined - } - - export async function resolvePluginSpec(plugin: PluginSpec, configFilepath: string): Promise { - const spec = pluginSpecifier(plugin) - if (!isPathPluginSpec(spec)) return plugin - - 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 - })() - - const resolved = await resolvePathPluginTarget(file).catch(() => file) - - if (Array.isArray(plugin)) return [resolved, plugin[1]] - return resolved - } - - export function deduplicatePluginOrigins(plugins: PluginOrigin[]): PluginOrigin[] { - const seen = new Set() - const list: PluginOrigin[] = [] - - 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) - } - - return list.toReversed() - } - - 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", +async function loadCommand(dir: string) { + const result: Record = {} + 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 - 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 + const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"] + const file = rel(item, patterns) ?? path.basename(item) + const name = trim(file) - 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", - }) + 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 }) + } + return result +} - export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote]) - export type Mcp = z.infer +async function loadAgent(dir: string) { + const result: Record = {} - export const PermissionAction = z.enum(["ask", "allow", "deny"]).meta({ - ref: "PermissionActionConfig", + 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 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 }) + } + return result +} + +async function loadMode(dir: string) { + const result: Record = {} + 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 + } + } + 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 +} + +export function pluginSpecifier(plugin: PluginSpec): string { + return Array.isArray(plugin) ? plugin[0] : plugin +} + +export function pluginOptions(plugin: PluginSpec): PluginOptions | undefined { + return Array.isArray(plugin) ? plugin[1] : undefined +} + +export async function resolvePluginSpec(plugin: PluginSpec, configFilepath: string): Promise { + const spec = pluginSpecifier(plugin) + if (!isPathPluginSpec(spec)) return plugin + + 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 + })() + + const resolved = await resolvePathPluginTarget(file).catch(() => file) + + if (Array.isArray(plugin)) return [resolved, plugin[1]] + return resolved +} + +export function deduplicatePluginOrigins(plugins: PluginOrigin[]): PluginOrigin[] { + const seen = new Set() + const list: PluginOrigin[] = [] + + 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) + } + + return list.toReversed() +} + +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 - - export const PermissionObject = z.record(z.string(), PermissionAction).meta({ - ref: "PermissionObjectConfig", + .strict() + .meta({ + ref: "McpLocalConfig", }) - export type PermissionObject = z.infer - 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)."), }) - export type PermissionRule = z.infer + .strict() + .meta({ + ref: "McpOAuthConfig", + }) +export type McpOAuth = z.infer - // 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 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 + +export const PermissionAction = z.enum(["ask", "allow", "deny"]).meta({ + ref: "PermissionActionConfig", +}) +export type PermissionAction = z.infer + +export const PermissionObject = z.record(z.string(), PermissionAction).meta({ + ref: "PermissionObjectConfig", +}) +export type PermissionObject = z.infer + +export const PermissionRule = z.union([PermissionAction, PermissionObject]).meta({ + ref: "PermissionRuleConfig", +}) +export type PermissionRule = z.infer + +// 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 +} - const permissionTransform = (x: unknown): Record => { - if (typeof x === "string") return { "*": x as PermissionAction } - const obj = x as { __originalKeys?: string[] } & Record - const { __originalKeys, ...rest } = obj - if (!__originalKeys) return rest as Record - const result: Record = {} - for (const key of __originalKeys) { - if (key in rest) result[key] = rest[key] as PermissionRule - } - return result +const permissionTransform = (x: unknown): Record => { + if (typeof x === "string") return { "*": x as PermissionAction } + const obj = x as { __originalKeys?: string[] } & Record + const { __originalKeys, ...rest } = obj + if (!__originalKeys) return rest as Record + const result: Record = {} + for (const key of __originalKeys) { + if (key in rest) result[key] = rest[key] as PermissionRule } + return result +} - 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 +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 - export const Command = z.object({ - template: z.string(), - description: z.string().optional(), - agent: z.string().optional(), +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 + +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 + +export const Agent = z + .object({ model: ModelId.optional(), - subtask: z.boolean().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(), }) - export type Command = z.infer + .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", + ]) - export const Skills = z.object({ - paths: z.array(z.string()).optional().describe("Additional paths to skill folders"), - urls: z + // Extract unknown properties into options + const options: Record = { ...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 + } + } + Object.assign(permission, agent.permission) + + // Convert legacy maxSteps to steps + const steps = agent.steps ?? agent.maxSteps + + return { ...agent, options, permission, steps } as typeof agent & { + options?: Record + permission?: Permission + steps?: number + } + }) + .meta({ + ref: "AgentConfig", + }) +export type Agent = z.infer + +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,q").describe("Exit the application"), + editor_open: z.string().optional().default("e").describe("Open external editor"), + theme_list: z.string().optional().default("t").describe("List available themes"), + sidebar_toggle: z.string().optional().default("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("s").describe("View status"), + session_export: z.string().optional().default("x").describe("Export session to editor"), + session_new: z.string().optional().default("n").describe("Create a new session"), + session_list: z.string().optional().default("l").describe("List all sessions"), + session_timeline: z.string().optional().default("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("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("y").describe("Copy message"), + messages_undo: z.string().optional().default("u").describe("Undo message"), + messages_redo: z.string().optional().default("r").describe("Redo message"), + messages_toggle_conceal: z + .string() + .optional() + .default("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("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("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("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("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 Layout = z.enum(["auto", "stretch"]).meta({ + ref: "LayoutConfig", +}) +export type Layout = z.infer + +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 Provider = z.infer + +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("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"), - }) - export type Skills = z.infer - - 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", - ]) - - // Extract unknown properties into options - const options: Record = { ...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 - } - } - Object.assign(permission, agent.permission) - - // Convert legacy maxSteps to steps - const steps = agent.steps ?? agent.maxSteps - - return { ...agent, options, permission, steps } as typeof agent & { - options?: Record - permission?: Permission - steps?: number - } - }) - .meta({ - ref: "AgentConfig", - }) - export type Agent = z.infer - - 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,q").describe("Exit the application"), - editor_open: z.string().optional().default("e").describe("Open external editor"), - theme_list: z.string().optional().default("t").describe("List available themes"), - sidebar_toggle: z.string().optional().default("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("s").describe("View status"), - session_export: z.string().optional().default("x").describe("Export session to editor"), - session_new: z.string().optional().default("n").describe("Create a new session"), - session_list: z.string().optional().default("l").describe("List all sessions"), - session_timeline: z.string().optional().default("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("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("y").describe("Copy message"), - messages_undo: z.string().optional().default("u").describe("Undo message"), - messages_redo: z.string().optional().default("r").describe("Redo message"), - messages_toggle_conceal: z - .string() - .optional() - .default("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("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("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("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("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 Layout = z.enum(["auto", "stretch"]).meta({ - ref: "LayoutConfig", - }) - export type Layout = z.infer - - 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), + .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 - - 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.", + 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(), + }), ), - 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( + ]) + .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(), - extensions: z.array(z.string()).optional(), + disabled: z.literal(true), }), - ), - ]) - .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.", - }, + 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(), + }), + ]), ), - 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)) - export type Info = z.output & { - plugin_origins?: PluginOrigin[] - } - - type State = { - config: Info - directories: string[] - deps: Fiber.Fiber[] - consoleState: ConsoleState - } - - export interface Interface { - readonly get: () => Effect.Effect - readonly getGlobal: () => Effect.Effect - readonly getConsoleState: () => Effect.Effect - readonly installDependencies: (dir: string, input?: InstallInput) => Effect.Effect - readonly update: (config: Info) => Effect.Effect - readonly updateGlobal: (config: Info) => Effect.Effect - readonly invalidate: (wait?: boolean) => Effect.Effect - readonly directories: () => Effect.Effect - readonly waitForDependencies: () => Effect.Effect - } - - export class Service extends Context.Service()("@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 patchJsonc(input: string, patch: unknown, path: string[] = []): string { - if (!isRecord(patch)) { - const edits = modify(input, path, patch, { - formattingOptions: { - insertSpaces: true, - tabSize: 2, + 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"), }) - return applyEdits(input, edits) - } + .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", + }) - return Object.entries(patch).reduce((result, [key, value]) => { - if (value === undefined) return result - return patchJsonc(result, value, [...path, key]) - }, input) +export type Info = z.output & { + plugin_origins?: PluginOrigin[] +} + +type State = { + config: Info + directories: string[] + deps: Fiber.Fiber[] + consoleState: ConsoleState +} + +export interface Interface { + readonly get: () => Effect.Effect + readonly getGlobal: () => Effect.Effect + readonly getConsoleState: () => Effect.Effect + readonly installDependencies: (dir: string, input?: InstallInput) => Effect.Effect + readonly update: (config: Info) => Effect.Effect + readonly updateGlobal: (config: Info) => Effect.Effect + readonly invalidate: (wait?: boolean) => Effect.Effect + readonly directories: () => Effect.Effect + readonly waitForDependencies: () => Effect.Effect +} + +export class Service extends Context.Service()("@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 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] +function writable(info: Info) { + const { plugin_origins, ...next } = info + return next +} - const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}` - if (!problemLine) return error +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] - return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^` - }) - .join("\n") + const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}` + if (!problemLine) return error - throw new JsonError({ - path: filepath, - message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`, + 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 = + Layer.effect( Service, Effect.gen(function* () { const fs = yield* AppFileSystem.Service @@ -1531,9 +1511,9 @@ export namespace Config { } if (result.tools) { - const perms: Record = {} + const perms: Record = {} 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 0000000000..60e39c3163 --- /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 a373b4d800..fd5cd8c88d 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 e64b226c14..163bd4d7d7 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 257922dafe..54139eb777 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 8737045c18..74966fd47a 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 1aeb2e51a4..595bb7a608 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 0c83890e55..4daacd30b8 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 a68c6c1d8d..cbaa2c24b3 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 44a9f3b430..6f020576d9 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 b6a44e2582..71d321080a 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 9f618eff8c..f31e0b9ff2 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 634fe6aad0..12617f9010 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 c029e5c5c6..1dd6027db9 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 aa770726df..11845c69c9 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 e8e46b2e3b..6e1a47ed20 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 d462a07f74..b69f35a649 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 f1c8701c4e..695008fc4e 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 6988d56e4e..b9e39d4eff 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 4978ef5478..810b949743 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 b4794ba5b1..23dd88ff5a 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 05d7882757..2efe4a4054 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 f0e52565d8..c4c6d09279 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 b02e7cc81c..d91b1427b0 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 08210de8a1..0a673f81c6 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 ad247f5466..667e0720c4 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 79b426c69c..4bf5d0cfed 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 9378e309aa..83963e3511 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 6900feecc3..2e9971ad71 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 ce99ab2992..bbb07caa40 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 af9565cba8..d77782354c 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 ed7e689da4..88957c6141 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 529d88bce1..4767e94b01 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 0c8968d94b..0c23550083 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 7970543547..fd7f5e3808 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 d415d23ebc..3c53314b6a 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 251447762d..1174cdf6a1 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 d384513087..10945be188 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 31727e3df9..ec1a87e969 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 80d74c7565..a0ea47c89c 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 fd230f5459..135d44db09 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 e7a143c9af..bc90dc0f22 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"