mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-21 13:21:17 +08:00
config: refactor
This commit is contained in:
@@ -44,6 +44,7 @@ import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Installation } from "@/installation"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { Config } from "@/config"
|
||||
import { ConfigMCP } from "@/config/mcp"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { z } from "zod"
|
||||
import { LoadAPIKeyError } from "ai"
|
||||
@@ -1213,7 +1214,7 @@ export namespace ACP {
|
||||
description: "compact the session",
|
||||
})
|
||||
|
||||
const mcpServers: Record<string, Config.Mcp> = {}
|
||||
const mcpServers: Record<string, ConfigMCP.Info> = {}
|
||||
for (const server of params.mcpServers) {
|
||||
if ("type" in server) {
|
||||
mcpServers[server.name] = {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MCP } from "../../mcp"
|
||||
import { McpAuth } from "../../mcp/auth"
|
||||
import { McpOAuthProvider } from "../../mcp/oauth-provider"
|
||||
import { Config } from "../../config"
|
||||
import { ConfigMCP } from "../../config/mcp"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Installation } from "../../installation"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
@@ -43,7 +44,7 @@ function getAuthStatusText(status: MCP.AuthStatus): string {
|
||||
|
||||
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
|
||||
|
||||
type McpConfigured = Config.Mcp
|
||||
type McpConfigured = ConfigMCP.Info
|
||||
function isMcpConfigured(config: McpEntry): config is McpConfigured {
|
||||
return typeof config === "object" && config !== null && "type" in config
|
||||
}
|
||||
@@ -426,7 +427,7 @@ async function resolveConfigPath(baseDir: string, global = false) {
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
|
||||
async function addMcpToConfig(name: string, mcpConfig: ConfigMCP.Info, configPath: string) {
|
||||
let text = "{}"
|
||||
if (await Filesystem.exists(configPath)) {
|
||||
text = await Filesystem.readText(configPath)
|
||||
@@ -514,7 +515,7 @@ export const McpAddCommand = cmd({
|
||||
})
|
||||
if (prompts.isCancel(command)) throw new UI.CancelledError()
|
||||
|
||||
const mcpConfig: Config.Mcp = {
|
||||
const mcpConfig: ConfigMCP.Info = {
|
||||
type: "local",
|
||||
command: command.split(" "),
|
||||
}
|
||||
@@ -544,7 +545,7 @@ export const McpAddCommand = cmd({
|
||||
})
|
||||
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
|
||||
|
||||
let mcpConfig: Config.Mcp
|
||||
let mcpConfig: ConfigMCP.Info
|
||||
|
||||
if (useOAuth) {
|
||||
const hasClientId = await prompts.confirm({
|
||||
|
||||
171
packages/opencode/src/config/agent.ts
Normal file
171
packages/opencode/src/config/agent.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
export * as ConfigAgent from "./agent"
|
||||
|
||||
import { Log } from "../util"
|
||||
import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
import { Bus } from "@/bus"
|
||||
import { configEntryNameFromPath } from "./entry-name"
|
||||
import * as ConfigMarkdown from "./markdown"
|
||||
import { ConfigModelID } from "./model-id"
|
||||
import { InvalidError } from "./paths"
|
||||
import { ConfigPermission } from "./permission"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
model: ConfigModelID.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: ConfigPermission.Info.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",
|
||||
])
|
||||
|
||||
const options: Record<string, unknown> = { ...agent.options }
|
||||
for (const [key, value] of Object.entries(agent)) {
|
||||
if (!knownKeys.has(key)) options[key] = value
|
||||
}
|
||||
|
||||
const permission: ConfigPermission.Info = {}
|
||||
for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
|
||||
const action = enabled ? "allow" : "deny"
|
||||
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
|
||||
permission.edit = action
|
||||
continue
|
||||
}
|
||||
permission[tool] = action
|
||||
}
|
||||
Object.assign(permission, agent.permission)
|
||||
|
||||
const steps = agent.steps ?? agent.maxSteps
|
||||
|
||||
return { ...agent, options, permission, steps } as typeof agent & {
|
||||
options?: Record<string, unknown>
|
||||
permission?: ConfigPermission.Info
|
||||
steps?: number
|
||||
}
|
||||
})
|
||||
.meta({
|
||||
ref: "AgentConfig",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export async function load(dir: string) {
|
||||
const result: Record<string, Info> = {}
|
||||
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")
|
||||
void 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 name = configEntryNameFromPath(item, patterns)
|
||||
|
||||
const config = {
|
||||
name,
|
||||
...md.data,
|
||||
prompt: md.content.trim(),
|
||||
}
|
||||
const parsed = Info.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 async function loadMode(dir: string) {
|
||||
const result: Record<string, Info> = {}
|
||||
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")
|
||||
void 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: configEntryNameFromPath(item, []),
|
||||
...md.data,
|
||||
prompt: md.content.trim(),
|
||||
}
|
||||
const parsed = Info.safeParse(config)
|
||||
if (parsed.success) {
|
||||
result[config.name] = {
|
||||
...parsed.data,
|
||||
mode: "primary" as const,
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,76 +1,60 @@
|
||||
export * as ConfigCommand from "./command"
|
||||
|
||||
import { Log } from "../util"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
import { Bus } from "@/bus"
|
||||
import { configEntryNameFromPath } from "./entry-name"
|
||||
import * as ConfigMarkdown from "./markdown"
|
||||
import { ConfigModelID } from "./model-id"
|
||||
import { InvalidError } from "./paths"
|
||||
|
||||
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
export const Info = z.object({
|
||||
template: z.string(),
|
||||
description: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
model: ConfigModelID.optional(),
|
||||
subtask: z.boolean().optional(),
|
||||
})
|
||||
|
||||
function trim(file: string) {
|
||||
const ext = path.extname(file)
|
||||
return ext.length ? file.slice(0, -ext.length) : file
|
||||
}
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export namespace ConfigCommand {
|
||||
export const Info = z.object({
|
||||
template: z.string(),
|
||||
description: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
model: ModelId.optional(),
|
||||
subtask: z.boolean().optional(),
|
||||
})
|
||||
export async function load(dir: string) {
|
||||
const result: Record<string, Info> = {}
|
||||
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")
|
||||
void 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 type Info = z.infer<typeof Info>
|
||||
const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"]
|
||||
const name = configEntryNameFromPath(item, patterns)
|
||||
|
||||
export async function load(dir: string) {
|
||||
const result: Record<string, Info> = {}
|
||||
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")
|
||||
void 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 = Info.safeParse(config)
|
||||
if (parsed.success) {
|
||||
result[config.name] = parsed.data
|
||||
continue
|
||||
}
|
||||
throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
|
||||
const config = {
|
||||
name,
|
||||
...md.data,
|
||||
template: md.content.trim(),
|
||||
}
|
||||
return result
|
||||
const parsed = Info.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
|
||||
}
|
||||
|
||||
@@ -20,12 +20,9 @@ import {
|
||||
import { Instance, type InstanceContext } from "../project/instance"
|
||||
import * as LSPServer from "../lsp/server"
|
||||
import { InstallationLocal, InstallationVersion } from "@/installation/version"
|
||||
import * as ConfigMarkdown from "./markdown"
|
||||
import { existsSync } from "fs"
|
||||
import { Bus } from "@/bus"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Event } from "../server/event"
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
import { Account } from "@/account"
|
||||
import { isRecord } from "@/util/record"
|
||||
import * as ConfigPaths from "./paths"
|
||||
@@ -36,22 +33,13 @@ import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect"
|
||||
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { Npm } from "@opencode-ai/shared/npm"
|
||||
import { ConfigAgent } from "./agent"
|
||||
import { ConfigMCP } from "./mcp"
|
||||
import { ConfigModelID } from "./model-id"
|
||||
import { ConfigPlugin } from "./plugin"
|
||||
import { ConfigManaged } from "./managed"
|
||||
import { ConfigCommand } from "./command"
|
||||
|
||||
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
|
||||
const PluginOptions = z.record(z.string(), z.unknown())
|
||||
export const PluginSpec = z.union([z.string(), z.tuple([z.string(), PluginOptions])])
|
||||
|
||||
export type PluginOptions = z.infer<typeof PluginOptions>
|
||||
export type PluginSpec = z.infer<typeof PluginSpec>
|
||||
export type PluginScope = "global" | "local"
|
||||
export type PluginOrigin = {
|
||||
spec: PluginSpec
|
||||
source: string
|
||||
scope: PluginScope
|
||||
}
|
||||
import { ConfigPermission } from "./permission"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
@@ -64,231 +52,6 @@ function mergeConfigConcatArrays(target: Info, source: Info): Info {
|
||||
return merged
|
||||
}
|
||||
|
||||
export type InstallInput = {
|
||||
waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise<void>
|
||||
}
|
||||
|
||||
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 loadAgent(dir: string) {
|
||||
const result: Record<string, Agent> = {}
|
||||
|
||||
for (const item of await Glob.scan("{agent,agents}/**/*.md", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse agent ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
void 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<string, Agent> = {}
|
||||
for (const item of await Glob.scan("{mode,modes}/*.md", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
? err.data.message
|
||||
: `Failed to parse mode ${item}`
|
||||
const { Session } = await import("@/session")
|
||||
void 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
|
||||
}
|
||||
|
||||
export const McpLocal = z
|
||||
.object({
|
||||
type: z.literal("local").describe("Type of MCP server connection"),
|
||||
command: z.string().array().describe("Command and arguments to run the MCP server"),
|
||||
environment: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("Environment variables to set when running the MCP server"),
|
||||
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
||||
timeout: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
ref: "McpLocalConfig",
|
||||
})
|
||||
|
||||
export const McpOAuth = z
|
||||
.object({
|
||||
clientId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."),
|
||||
clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"),
|
||||
scope: z.string().optional().describe("OAuth scopes to request during authorization"),
|
||||
redirectUri: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
ref: "McpOAuthConfig",
|
||||
})
|
||||
export type McpOAuth = z.infer<typeof McpOAuth>
|
||||
|
||||
export const McpRemote = z
|
||||
.object({
|
||||
type: z.literal("remote").describe("Type of MCP server connection"),
|
||||
url: z.string().describe("URL of the remote MCP server"),
|
||||
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
||||
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
|
||||
oauth: z
|
||||
.union([McpOAuth, z.literal(false)])
|
||||
.optional()
|
||||
.describe("OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection."),
|
||||
timeout: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
ref: "McpRemoteConfig",
|
||||
})
|
||||
|
||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||
export type Mcp = z.infer<typeof Mcp>
|
||||
|
||||
export const PermissionAction = z.enum(["ask", "allow", "deny"]).meta({
|
||||
ref: "PermissionActionConfig",
|
||||
})
|
||||
export type PermissionAction = z.infer<typeof PermissionAction>
|
||||
|
||||
export const PermissionObject = z.record(z.string(), PermissionAction).meta({
|
||||
ref: "PermissionObjectConfig",
|
||||
})
|
||||
export type PermissionObject = z.infer<typeof PermissionObject>
|
||||
|
||||
export const PermissionRule = z.union([PermissionAction, PermissionObject]).meta({
|
||||
ref: "PermissionRuleConfig",
|
||||
})
|
||||
export type PermissionRule = z.infer<typeof PermissionRule>
|
||||
|
||||
// Capture original key order before zod reorders, then rebuild in original order
|
||||
const permissionPreprocess = (val: unknown) => {
|
||||
if (typeof val === "object" && val !== null && !Array.isArray(val)) {
|
||||
return { __originalKeys: Object.keys(val), ...val }
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
const permissionTransform = (x: unknown): Record<string, PermissionRule> => {
|
||||
if (typeof x === "string") return { "*": x as PermissionAction }
|
||||
const obj = x as { __originalKeys?: string[] } & Record<string, unknown>
|
||||
const { __originalKeys, ...rest } = obj
|
||||
if (!__originalKeys) return rest as Record<string, PermissionRule>
|
||||
const result: Record<string, PermissionRule> = {}
|
||||
for (const key of __originalKeys) {
|
||||
if (key in rest) result[key] = rest[key] as PermissionRule
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const Permission = z
|
||||
.preprocess(
|
||||
permissionPreprocess,
|
||||
z
|
||||
.object({
|
||||
__originalKeys: z.string().array().optional(),
|
||||
read: PermissionRule.optional(),
|
||||
edit: PermissionRule.optional(),
|
||||
glob: PermissionRule.optional(),
|
||||
grep: PermissionRule.optional(),
|
||||
list: PermissionRule.optional(),
|
||||
bash: PermissionRule.optional(),
|
||||
task: PermissionRule.optional(),
|
||||
external_directory: PermissionRule.optional(),
|
||||
todowrite: PermissionAction.optional(),
|
||||
question: PermissionAction.optional(),
|
||||
webfetch: PermissionAction.optional(),
|
||||
websearch: PermissionAction.optional(),
|
||||
codesearch: PermissionAction.optional(),
|
||||
lsp: PermissionRule.optional(),
|
||||
doom_loop: PermissionAction.optional(),
|
||||
skill: PermissionRule.optional(),
|
||||
})
|
||||
.catchall(PermissionRule)
|
||||
.or(PermissionAction),
|
||||
)
|
||||
.transform(permissionTransform)
|
||||
.meta({
|
||||
ref: "PermissionConfig",
|
||||
})
|
||||
export type Permission = z.infer<typeof Permission>
|
||||
|
||||
export const Skills = z.object({
|
||||
paths: z.array(z.string()).optional().describe("Additional paths to skill folders"),
|
||||
urls: z
|
||||
@@ -298,95 +61,6 @@ export const Skills = z.object({
|
||||
})
|
||||
export type Skills = z.infer<typeof Skills>
|
||||
|
||||
export const Agent = z
|
||||
.object({
|
||||
model: ModelId.optional(),
|
||||
variant: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Default model variant for this agent (applies only when using the agent's configured model)."),
|
||||
temperature: z.number().optional(),
|
||||
top_p: z.number().optional(),
|
||||
prompt: z.string().optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"),
|
||||
disable: z.boolean().optional(),
|
||||
description: z.string().optional().describe("Description of when to use the agent"),
|
||||
mode: z.enum(["subagent", "primary", "all"]).optional(),
|
||||
hidden: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
|
||||
options: z.record(z.string(), z.any()).optional(),
|
||||
color: z
|
||||
.union([
|
||||
z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"),
|
||||
z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
])
|
||||
.optional()
|
||||
.describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"),
|
||||
steps: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Maximum number of agentic iterations before forcing text-only response"),
|
||||
maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
|
||||
permission: Permission.optional(),
|
||||
})
|
||||
.catchall(z.any())
|
||||
.transform((agent, _ctx) => {
|
||||
const knownKeys = new Set([
|
||||
"name",
|
||||
"model",
|
||||
"variant",
|
||||
"prompt",
|
||||
"description",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"mode",
|
||||
"hidden",
|
||||
"color",
|
||||
"steps",
|
||||
"maxSteps",
|
||||
"options",
|
||||
"permission",
|
||||
"disable",
|
||||
"tools",
|
||||
])
|
||||
|
||||
// Extract unknown properties into options
|
||||
const options: Record<string, unknown> = { ...agent.options }
|
||||
for (const [key, value] of Object.entries(agent)) {
|
||||
if (!knownKeys.has(key)) options[key] = value
|
||||
}
|
||||
|
||||
// Convert legacy tools config to permissions
|
||||
const permission: Permission = {}
|
||||
for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
|
||||
const action = enabled ? "allow" : "deny"
|
||||
// write, edit, patch, multiedit all map to edit permission
|
||||
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
|
||||
permission.edit = action
|
||||
} else {
|
||||
permission[tool] = action
|
||||
}
|
||||
}
|
||||
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<string, unknown>
|
||||
permission?: Permission
|
||||
steps?: number
|
||||
}
|
||||
})
|
||||
.meta({
|
||||
ref: "AgentConfig",
|
||||
})
|
||||
export type Agent = z.infer<typeof Agent>
|
||||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
||||
@@ -696,7 +370,7 @@ export const Info = z
|
||||
.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(),
|
||||
plugin: ConfigPlugin.Spec.array().optional(),
|
||||
share: z
|
||||
.enum(["manual", "auto", "disabled"])
|
||||
.optional()
|
||||
@@ -718,8 +392,8 @@ export const Info = 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(
|
||||
model: ConfigModelID.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||
small_model: ConfigModelID.describe(
|
||||
"Small model to use for tasks like title generation in the format of provider/model",
|
||||
).optional(),
|
||||
default_agent: z
|
||||
@@ -731,26 +405,26 @@ export const Info = z
|
||||
username: z.string().optional().describe("Custom username to display in conversations instead of system username"),
|
||||
mode: z
|
||||
.object({
|
||||
build: Agent.optional(),
|
||||
plan: Agent.optional(),
|
||||
build: ConfigAgent.Info.optional(),
|
||||
plan: ConfigAgent.Info.optional(),
|
||||
})
|
||||
.catchall(Agent)
|
||||
.catchall(ConfigAgent.Info)
|
||||
.optional()
|
||||
.describe("@deprecated Use `agent` field instead."),
|
||||
agent: z
|
||||
.object({
|
||||
// primary
|
||||
plan: Agent.optional(),
|
||||
build: Agent.optional(),
|
||||
plan: ConfigAgent.Info.optional(),
|
||||
build: ConfigAgent.Info.optional(),
|
||||
// subagent
|
||||
general: Agent.optional(),
|
||||
explore: Agent.optional(),
|
||||
general: ConfigAgent.Info.optional(),
|
||||
explore: ConfigAgent.Info.optional(),
|
||||
// specialized
|
||||
title: Agent.optional(),
|
||||
summary: Agent.optional(),
|
||||
compaction: Agent.optional(),
|
||||
title: ConfigAgent.Info.optional(),
|
||||
summary: ConfigAgent.Info.optional(),
|
||||
compaction: ConfigAgent.Info.optional(),
|
||||
})
|
||||
.catchall(Agent)
|
||||
.catchall(ConfigAgent.Info)
|
||||
.optional()
|
||||
.describe("Agent configuration, see https://opencode.ai/docs/agents"),
|
||||
provider: z.record(z.string(), Provider).optional().describe("Custom provider configurations and model overrides"),
|
||||
@@ -758,7 +432,7 @@ export const Info = z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([
|
||||
Mcp,
|
||||
ConfigMCP.Info,
|
||||
z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
@@ -820,7 +494,7 @@ export const Info = z
|
||||
),
|
||||
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(),
|
||||
permission: ConfigPermission.Info.optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
enterprise: z
|
||||
.object({
|
||||
@@ -867,7 +541,7 @@ export const Info = z
|
||||
})
|
||||
|
||||
export type Info = z.output<typeof Info> & {
|
||||
plugin_origins?: PluginOrigin[]
|
||||
plugin_origins?: ConfigPlugin.Origin[]
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -1084,10 +758,17 @@ export const layer = Layer.effect(
|
||||
const gitignore = path.join(dir, ".gitignore")
|
||||
const hasIgnore = yield* fs.existsSafe(gitignore)
|
||||
if (!hasIgnore) {
|
||||
yield* fs.writeFileString(
|
||||
gitignore,
|
||||
["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"),
|
||||
)
|
||||
yield* fs
|
||||
.writeFileString(
|
||||
gitignore,
|
||||
["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"),
|
||||
)
|
||||
.pipe(
|
||||
Effect.catchIf(
|
||||
(e) => e.reason._tag === "PermissionDenied",
|
||||
() => Effect.void,
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1105,7 +786,11 @@ export const layer = Layer.effect(
|
||||
return "global"
|
||||
})
|
||||
|
||||
const track = Effect.fnUntraced(function* (source: string, list: PluginSpec[] | undefined, kind?: PluginScope) {
|
||||
const track = Effect.fnUntraced(function* (
|
||||
source: string,
|
||||
list: ConfigPlugin.Spec[] | undefined,
|
||||
kind?: ConfigPlugin.Scope,
|
||||
) {
|
||||
if (!list?.length) return
|
||||
const hit = kind ?? (yield* scope(source))
|
||||
const plugins = ConfigPlugin.deduplicatePluginOrigins([
|
||||
@@ -1116,7 +801,7 @@ export const layer = Layer.effect(
|
||||
result.plugin_origins = plugins
|
||||
})
|
||||
|
||||
const merge = (source: string, next: Info, kind?: PluginScope) => {
|
||||
const merge = (source: string, next: Info, kind?: ConfigPlugin.Scope) => {
|
||||
result = mergeConfigConcatArrays(result, next)
|
||||
return track(source, next.plugin, kind)
|
||||
}
|
||||
@@ -1183,7 +868,7 @@ export const layer = Layer.effect(
|
||||
}
|
||||
}
|
||||
|
||||
yield* ensureGitignore(dir).pipe(Effect.forkScoped)
|
||||
yield* ensureGitignore(dir).pipe(Effect.orDie)
|
||||
|
||||
const dep = yield* npmSvc
|
||||
.install(dir, {
|
||||
@@ -1204,8 +889,8 @@ export const layer = Layer.effect(
|
||||
deps.push(dep)
|
||||
|
||||
result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir)))
|
||||
result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadAgent(dir)))
|
||||
result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadMode(dir)))
|
||||
result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir)))
|
||||
result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir)))
|
||||
const list = yield* Effect.promise(() => ConfigPlugin.load(dir))
|
||||
yield* track(dir, list)
|
||||
}
|
||||
@@ -1284,9 +969,9 @@ export const layer = Layer.effect(
|
||||
}
|
||||
|
||||
if (result.tools) {
|
||||
const perms: Record<string, PermissionAction> = {}
|
||||
const perms: Record<string, ConfigPermission.Action> = {}
|
||||
for (const [tool, enabled] of Object.entries(result.tools)) {
|
||||
const action: PermissionAction = enabled ? "allow" : "deny"
|
||||
const action: ConfigPermission.Action = enabled ? "allow" : "deny"
|
||||
if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
|
||||
perms.edit = action
|
||||
continue
|
||||
|
||||
16
packages/opencode/src/config/entry-name.ts
Normal file
16
packages/opencode/src/config/entry-name.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import path from "path"
|
||||
|
||||
function sliceAfterMatch(filePath: string, searchRoots: string[]) {
|
||||
const normalizedPath = filePath.replaceAll("\\", "/")
|
||||
for (const searchRoot of searchRoots) {
|
||||
const index = normalizedPath.indexOf(searchRoot)
|
||||
if (index === -1) continue
|
||||
return normalizedPath.slice(index + searchRoot.length)
|
||||
}
|
||||
}
|
||||
|
||||
export function configEntryNameFromPath(filePath: string, searchRoots: string[]) {
|
||||
const candidate = sliceAfterMatch(filePath, searchRoots) ?? path.basename(filePath)
|
||||
const ext = path.extname(candidate)
|
||||
return ext.length ? candidate.slice(0, -ext.length) : candidate
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
export * as Config from "./config"
|
||||
export * as ConfigAgent from "./agent"
|
||||
export * as ConfigCommand from "./command"
|
||||
export { ConfigManaged } from "./managed"
|
||||
export * as ConfigMarkdown from "./markdown"
|
||||
export * as ConfigMCP from "./mcp"
|
||||
export { ConfigModelID } from "./model-id"
|
||||
export * as ConfigPermission from "./permission"
|
||||
export * as ConfigPaths from "./paths"
|
||||
|
||||
70
packages/opencode/src/config/mcp.ts
Normal file
70
packages/opencode/src/config/mcp.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import z from "zod"
|
||||
|
||||
export namespace ConfigMCP {
|
||||
export const Local = z
|
||||
.object({
|
||||
type: z.literal("local").describe("Type of MCP server connection"),
|
||||
command: z.string().array().describe("Command and arguments to run the MCP server"),
|
||||
environment: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("Environment variables to set when running the MCP server"),
|
||||
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
||||
timeout: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified."),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
ref: "McpLocalConfig",
|
||||
})
|
||||
|
||||
export const OAuth = 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 OAuth = z.infer<typeof OAuth>
|
||||
|
||||
export const Remote = 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([OAuth, 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 Info = z.discriminatedUnion("type", [Local, Remote])
|
||||
export type Info = z.infer<typeof Info>
|
||||
}
|
||||
3
packages/opencode/src/config/model-id.ts
Normal file
3
packages/opencode/src/config/model-id.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import z from "zod"
|
||||
|
||||
export const ConfigModelID = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
|
||||
68
packages/opencode/src/config/permission.ts
Normal file
68
packages/opencode/src/config/permission.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export * as ConfigPermission from "./permission"
|
||||
import z from "zod"
|
||||
|
||||
const permissionPreprocess = (val: unknown) => {
|
||||
if (typeof val === "object" && val !== null && !Array.isArray(val)) {
|
||||
return { __originalKeys: globalThis.Object.keys(val), ...val }
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
export const Action = z.enum(["ask", "allow", "deny"]).meta({
|
||||
ref: "PermissionActionConfig",
|
||||
})
|
||||
export type Action = z.infer<typeof Action>
|
||||
|
||||
export const Object = z.record(z.string(), Action).meta({
|
||||
ref: "PermissionObjectConfig",
|
||||
})
|
||||
export type Object = z.infer<typeof Object>
|
||||
|
||||
export const Rule = z.union([Action, Object]).meta({
|
||||
ref: "PermissionRuleConfig",
|
||||
})
|
||||
export type Rule = z.infer<typeof Rule>
|
||||
|
||||
const transform = (x: unknown): Record<string, Rule> => {
|
||||
if (typeof x === "string") return { "*": x as Action }
|
||||
const obj = x as { __originalKeys?: string[] } & Record<string, unknown>
|
||||
const { __originalKeys, ...rest } = obj
|
||||
if (!__originalKeys) return rest as Record<string, Rule>
|
||||
const result: Record<string, Rule> = {}
|
||||
for (const key of __originalKeys) {
|
||||
if (key in rest) result[key] = rest[key] as Rule
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const Info = z
|
||||
.preprocess(
|
||||
permissionPreprocess,
|
||||
z
|
||||
.object({
|
||||
__originalKeys: z.string().array().optional(),
|
||||
read: Rule.optional(),
|
||||
edit: Rule.optional(),
|
||||
glob: Rule.optional(),
|
||||
grep: Rule.optional(),
|
||||
list: Rule.optional(),
|
||||
bash: Rule.optional(),
|
||||
task: Rule.optional(),
|
||||
external_directory: Rule.optional(),
|
||||
todowrite: Action.optional(),
|
||||
question: Action.optional(),
|
||||
webfetch: Action.optional(),
|
||||
websearch: Action.optional(),
|
||||
codesearch: Action.optional(),
|
||||
lsp: Rule.optional(),
|
||||
doom_loop: Action.optional(),
|
||||
skill: Rule.optional(),
|
||||
})
|
||||
.catchall(Rule)
|
||||
.or(Action),
|
||||
)
|
||||
.transform(transform)
|
||||
.meta({
|
||||
ref: "PermissionConfig",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ToolListChangedNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
import { Config } from "../config"
|
||||
import { ConfigMCP } from "../config/mcp"
|
||||
import { Log } from "../util"
|
||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
||||
import z from "zod/v4"
|
||||
@@ -123,7 +124,7 @@ type PromptInfo = Awaited<ReturnType<MCPClient["listPrompts"]>>["prompts"][numbe
|
||||
type ResourceInfo = Awaited<ReturnType<MCPClient["listResources"]>>["resources"][number]
|
||||
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
|
||||
|
||||
function isMcpConfigured(entry: McpEntry): entry is Config.Mcp {
|
||||
function isMcpConfigured(entry: McpEntry): entry is ConfigMCP.Info {
|
||||
return typeof entry === "object" && entry !== null && "type" in entry
|
||||
}
|
||||
|
||||
@@ -224,7 +225,7 @@ export interface Interface {
|
||||
readonly tools: () => Effect.Effect<Record<string, Tool>>
|
||||
readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
|
||||
readonly resources: () => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
|
||||
readonly add: (name: string, mcp: Config.Mcp) => Effect.Effect<{ status: Record<string, Status> | Status }>
|
||||
readonly add: (name: string, mcp: ConfigMCP.Info) => Effect.Effect<{ status: Record<string, Status> | Status }>
|
||||
readonly connect: (name: string) => Effect.Effect<void>
|
||||
readonly disconnect: (name: string) => Effect.Effect<void>
|
||||
readonly getPrompt: (
|
||||
@@ -276,7 +277,10 @@ export const layer = Layer.effect(
|
||||
|
||||
const DISABLED_RESULT: CreateResult = { status: { status: "disabled" } }
|
||||
|
||||
const connectRemote = Effect.fn("MCP.connectRemote")(function* (key: string, mcp: Config.Mcp & { type: "remote" }) {
|
||||
const connectRemote = Effect.fn("MCP.connectRemote")(function* (
|
||||
key: string,
|
||||
mcp: ConfigMCP.Info & { type: "remote" },
|
||||
) {
|
||||
const oauthDisabled = mcp.oauth === false
|
||||
const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined
|
||||
let authProvider: McpOAuthProvider | undefined
|
||||
@@ -382,7 +386,10 @@ export const layer = Layer.effect(
|
||||
}
|
||||
})
|
||||
|
||||
const connectLocal = Effect.fn("MCP.connectLocal")(function* (key: string, mcp: Config.Mcp & { type: "local" }) {
|
||||
const connectLocal = Effect.fn("MCP.connectLocal")(function* (
|
||||
key: string,
|
||||
mcp: ConfigMCP.Info & { type: "local" },
|
||||
) {
|
||||
const [cmd, ...args] = mcp.command
|
||||
const cwd = Instance.directory
|
||||
const transport = new StdioClientTransport({
|
||||
@@ -414,7 +421,7 @@ export const layer = Layer.effect(
|
||||
)
|
||||
})
|
||||
|
||||
const create = Effect.fn("MCP.create")(function* (key: string, mcp: Config.Mcp) {
|
||||
const create = Effect.fn("MCP.create")(function* (key: string, mcp: ConfigMCP.Info) {
|
||||
if (mcp.enabled === false) {
|
||||
log.info("mcp server disabled", { key })
|
||||
return DISABLED_RESULT
|
||||
@@ -424,8 +431,8 @@ export const layer = Layer.effect(
|
||||
|
||||
const { client: mcpClient, status } =
|
||||
mcp.type === "remote"
|
||||
? yield* connectRemote(key, mcp as Config.Mcp & { type: "remote" })
|
||||
: yield* connectLocal(key, mcp as Config.Mcp & { type: "local" })
|
||||
? yield* connectRemote(key, mcp as ConfigMCP.Info & { type: "remote" })
|
||||
: yield* connectLocal(key, mcp as ConfigMCP.Info & { type: "local" })
|
||||
|
||||
if (!mcpClient) {
|
||||
return { status } satisfies CreateResult
|
||||
@@ -588,7 +595,7 @@ export const layer = Layer.effect(
|
||||
return s.clients
|
||||
})
|
||||
|
||||
const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: Config.Mcp) {
|
||||
const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCP.Info) {
|
||||
const s = yield* InstanceState.get(state)
|
||||
const result = yield* create(name, mcp)
|
||||
|
||||
@@ -602,7 +609,7 @@ export const layer = Layer.effect(
|
||||
return yield* storeClient(s, name, result.mcpClient, result.defs!, mcp.timeout)
|
||||
})
|
||||
|
||||
const add = Effect.fn("MCP.add")(function* (name: string, mcp: Config.Mcp) {
|
||||
const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCP.Info) {
|
||||
yield* createAndStore(name, mcp)
|
||||
const s = yield* InstanceState.get(state)
|
||||
return { status: s.status }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Bus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Config } from "@/config"
|
||||
import { ConfigPermission } from "@/config/permission"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { MessageID, SessionID } from "@/session/schema"
|
||||
@@ -289,7 +289,7 @@ function expand(pattern: string): string {
|
||||
return pattern
|
||||
}
|
||||
|
||||
export function fromConfig(permission: Config.Permission) {
|
||||
export function fromConfig(permission: ConfigPermission.Info) {
|
||||
const ruleset: Ruleset = []
|
||||
for (const [key, value] of Object.entries(permission)) {
|
||||
if (typeof value === "string") {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describeRoute, validator, resolver } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { MCP } from "../../mcp"
|
||||
import { Config } from "../../config"
|
||||
import { ConfigMCP } from "../../config/mcp"
|
||||
import { AppRuntime } from "../../effect/app-runtime"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
@@ -53,7 +54,7 @@ export const McpRoutes = lazy(() =>
|
||||
"json",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
config: Config.Mcp,
|
||||
config: ConfigMCP.Info,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
|
||||
@@ -845,6 +845,9 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: this is a hack to wait for backgruounded gitignore
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
expect(await Filesystem.exists(path.join(tmp.extra, ".gitignore"))).toBe(true)
|
||||
expect(await Filesystem.readText(path.join(tmp.extra, ".gitignore"))).toContain("package-lock.json")
|
||||
} finally {
|
||||
@@ -1865,7 +1868,7 @@ describe("resolvePluginSpec", () => {
|
||||
})
|
||||
|
||||
describe("deduplicatePluginOrigins", () => {
|
||||
const dedupe = (plugins: Config.PluginSpec[]) =>
|
||||
const dedupe = (plugins: ConfigPlugin.Spec[]) =>
|
||||
ConfigPlugin.deduplicatePluginOrigins(
|
||||
plugins.map((spec) => ({
|
||||
spec,
|
||||
|
||||
Reference in New Issue
Block a user