From 939da6a5e70167dfdbf8593fdd68ce4c46474723 Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 6 Aug 2026 10:35:48 -0700 Subject: [PATCH] feat(cli): add debug config command (#40895) --- packages/cli/src/commands/commands.ts | 5 +- .../cli/src/commands/handlers/debug/config.ts | 19 + packages/cli/src/index.ts | 1 + packages/cli/test/debug-config.test.ts | 83 + packages/client/script/build.ts | 2 + packages/client/src/effect/api/api.ts | 12 + .../client/src/effect/generated/client.ts | 10 + packages/client/src/effect/index.ts | 2 + packages/client/src/promise/api.ts | 1 + .../client/src/promise/generated/client.ts | 16 + .../client/src/promise/generated/types.ts | 182 ++ packages/client/src/promise/index.ts | 1 + .../client/test/contract-identity.test.ts | 2 + packages/client/test/promise.test.ts | 29 + packages/core/src/config.ts | 168 +- packages/core/src/config/plugin/agent.ts | 13 +- packages/core/src/config/plugin/command.ts | 7 +- packages/core/src/config/plugin/policy.ts | 3 +- packages/core/src/config/plugin/provider.ts | 5 +- packages/core/src/config/plugin/reference.ts | 5 +- .../core/src/filesystem/location-watcher.ts | 3 +- packages/core/src/mcp/client.ts | 2 +- packages/core/src/mcp/index.ts | 5 +- packages/core/src/mcp/oauth.ts | 2 +- packages/core/src/plugin/supervisor.ts | 14 +- packages/core/src/session/compaction.ts | 5 +- packages/core/src/v1/config/config.ts | 2 +- packages/core/test/config/agent.test.ts | 19 +- packages/core/test/config/command.test.ts | 12 +- packages/core/test/config/config.test.ts | 70 +- packages/core/test/config/model.test.ts | 2 +- packages/core/test/config/policy.test.ts | 10 +- packages/core/test/config/provider.test.ts | 21 +- packages/core/test/config/reload.test.ts | 8 +- packages/core/test/config/skill.test.ts | 11 +- packages/core/test/config/warming.test.ts | 4 +- packages/core/test/formatter.test.ts | 7 +- packages/core/test/mcp.test.ts | 7 +- packages/core/test/pty/pty-session.test.ts | 3 +- packages/core/test/session-runner.test.ts | 11 +- packages/core/test/tool-read.test.ts | 15 +- packages/protocol/openapi.json | 2070 +++++++++++++++-- packages/protocol/src/api.ts | 3 + packages/protocol/src/client.ts | 1 + packages/protocol/src/groups/config.ts | 22 + packages/schema/src/config.ts | 136 ++ packages/{core => schema}/src/config/agent.ts | 10 +- .../{core => schema}/src/config/command.ts | 4 +- .../{core => schema}/src/config/compaction.ts | 4 +- .../src/config/experimental.ts | 6 +- .../{core => schema}/src/config/formatter.ts | 2 +- packages/{core => schema}/src/config/lsp.ts | 2 +- packages/{core => schema}/src/config/mcp.ts | 6 +- packages/{core => schema}/src/config/media.ts | 4 +- packages/{core => schema}/src/config/model.ts | 6 +- .../{core => schema}/src/config/plugin.ts | 2 +- .../{core => schema}/src/config/policy.ts | 2 +- .../{core => schema}/src/config/provider.ts | 6 +- .../{core => schema}/src/config/reference.ts | 2 +- .../src/config/tool-output.ts | 4 +- .../{core => schema}/src/config/warming.ts | 2 +- .../{core => schema}/src/config/watcher.ts | 2 +- .../{core => schema}/src/config/websearch.ts | 4 +- packages/schema/test/config.test.ts | 42 + packages/sdk-next/src/index.ts | 1 + .../sdk-next/test/contract-identity.test.ts | 3 + packages/server/src/handlers.ts | 2 + packages/server/src/handlers/config.ts | 7 + packages/server/test/config.test.ts | 62 + packages/www/openapi.json | 2070 +++++++++++++++-- packages/www/public/openapi.json | 2070 +++++++++++++++-- 71 files changed, 6653 insertions(+), 703 deletions(-) create mode 100644 packages/cli/src/commands/handlers/debug/config.ts create mode 100644 packages/cli/test/debug-config.test.ts create mode 100644 packages/protocol/src/groups/config.ts rename packages/{core => schema}/src/config/agent.ts (76%) rename packages/{core => schema}/src/config/command.ts (79%) rename packages/{core => schema}/src/config/compaction.ts (78%) rename packages/{core => schema}/src/config/experimental.ts (74%) rename packages/{core => schema}/src/config/formatter.ts (90%) rename packages/{core => schema}/src/config/lsp.ts (94%) rename packages/{core => schema}/src/config/mcp.ts (71%) rename packages/{core => schema}/src/config/media.ts (83%) rename packages/{core => schema}/src/config/model.ts (87%) rename packages/{core => schema}/src/config/plugin.ts (89%) rename packages/{core => schema}/src/config/policy.ts (86%) rename packages/{core => schema}/src/config/provider.ts (95%) rename packages/{core => schema}/src/config/reference.ts (93%) rename packages/{core => schema}/src/config/tool-output.ts (68%) rename packages/{core => schema}/src/config/warming.ts (93%) rename packages/{core => schema}/src/config/watcher.ts (78%) rename packages/{core => schema}/src/config/websearch.ts (56%) create mode 100644 packages/schema/test/config.test.ts create mode 100644 packages/server/src/handlers/config.ts create mode 100644 packages/server/test/config.test.ts diff --git a/packages/cli/src/commands/commands.ts b/packages/cli/src/commands/commands.ts index 7f5792d459c..df1068ac50d 100644 --- a/packages/cli/src/commands/commands.ts +++ b/packages/cli/src/commands/commands.ts @@ -68,7 +68,10 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO }), Spec.make("debug", { description: "Debugging and troubleshooting tools", - commands: [Spec.make("agents", { description: "List all agents" })], + commands: [ + Spec.make("agents", { description: "List all agents" }), + Spec.make("config", { description: "Show resolved configuration" }), + ], }), Spec.make("console", { description: "Manage OpenCode Console access", diff --git a/packages/cli/src/commands/handlers/debug/config.ts b/packages/cli/src/commands/handlers/debug/config.ts new file mode 100644 index 00000000000..d6ff940e764 --- /dev/null +++ b/packages/cli/src/commands/handlers/debug/config.ts @@ -0,0 +1,19 @@ +import { EOL } from "os" +import { Effect } from "effect" +import { OpenCode } from "@opencode-ai/client" +import { Service } from "@opencode-ai/client/effect/service" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { ServiceConfig } from "../../../services/service-config" + +export default Runtime.handler( + Commands.commands.debug.commands.config, + Effect.fn("cli.debug.config")(function* () { + const options = yield* ServiceConfig.options() + const found = yield* Service.discover(options) + const endpoint = found ?? (yield* Service.ensure(options)) + const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }) + const entries = yield* Effect.promise(() => client.config.get({ location: { directory: process.cwd() } })) + process.stdout.write(JSON.stringify(entries, null, 2) + EOL) + }), +) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ea7fa6ba4c7..82361c706b1 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -22,6 +22,7 @@ const Handlers = Runtime.handlers(Commands, { }, debug: { agents: () => import("./commands/handlers/debug/agents"), + config: () => import("./commands/handlers/debug/config"), }, console: { login: () => import("./commands/handlers/console/login"), diff --git a/packages/cli/test/debug-config.test.ts b/packages/cli/test/debug-config.test.ts new file mode 100644 index 00000000000..b7816ba650b --- /dev/null +++ b/packages/cli/test/debug-config.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, test } from "bun:test" +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { OPENCODE_VERSION } from "../src/version" + +describe("debug config command", () => { + test("is included in troubleshooting help", async () => { + const [debug, config] = await Promise.all([cli(["debug", "--help"]), cli(["debug", "config", "--help"])]) + + expect(debug.exitCode).toBe(0) + expect(debug.stdout).toContain("config") + expect(debug.stdout).toContain("Show resolved configuration") + expect(config.exitCode).toBe(0) + expect(config.stdout).toContain("opencode debug config [flags]") + }) + + test("prints config entries from the invoking directory without reordering permissions", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-debug-config-")) + const project = path.join(import.meta.dir, "..") + const registration = path.join(root, "state", "opencode", "service-local.json") + const entries = [ + { + type: "document", + path: path.join(project, "opencode.json"), + info: { + permissions: [ + { action: "shell", resource: "*", effect: "ask" }, + { action: "shell", resource: "git status", effect: "allow" }, + ], + }, + }, + { type: "file", path: path.join(project, "opencode.json") }, + ] + let requested: URL | undefined + const authorization: Array = [] + const server = Bun.serve({ + port: 0, + fetch(request) { + const url = new URL(request.url) + if (url.pathname === "/api/health") { + return Response.json({ healthy: true, version: OPENCODE_VERSION, pid: process.pid }) + } + requested = url + authorization.push(request.headers.get("authorization")) + return Response.json(entries) + }, + }) + + try { + await fs.mkdir(path.dirname(registration), { recursive: true }) + await fs.writeFile( + registration, + JSON.stringify({ version: OPENCODE_VERSION, url: server.url.toString(), pid: process.pid, password: "secret" }), + ) + const result = await cli(["debug", "config"], project, { XDG_STATE_HOME: path.join(root, "state") }) + + expect({ exitCode: result.exitCode, stderr: result.stderr }).toEqual({ exitCode: 0, stderr: "" }) + expect(JSON.parse(result.stdout)).toEqual(entries) + expect(requested?.pathname).toBe("/api/config") + expect(requested?.searchParams.get("location[directory]")).toBe(project) + expect(authorization).toEqual([`Basic ${btoa("opencode:secret")}`]) + } finally { + server.stop(true) + await fs.rm(root, { recursive: true, force: true }) + } + }) +}) + +async function cli(args: string[], cwd = path.join(import.meta.dir, ".."), env?: Record) { + const child = Bun.spawn([process.execPath, "run", path.join(import.meta.dir, "../src/index.ts"), ...args], { + cwd, + env: { ...process.env, ...env }, + stdout: "pipe", + stderr: "pipe", + }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + return { stdout, stderr, exitCode } +} diff --git a/packages/client/script/build.ts b/packages/client/script/build.ts index 6f0dbaad99f..5c452c984b6 100644 --- a/packages/client/script/build.ts +++ b/packages/client/script/build.ts @@ -8,6 +8,7 @@ import { } from "@opencode-ai/protocol/client" import { Agent } from "@opencode-ai/schema/agent" import { Command } from "@opencode-ai/schema/command" +import { Config } from "@opencode-ai/schema/config" import { Credential } from "@opencode-ai/schema/credential" import { Event } from "@opencode-ai/schema/event" import { EventLog } from "@opencode-ai/schema/event-log" @@ -48,6 +49,7 @@ const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmi const effectTypeReferences = [ ...namespaceTypes("Agent", "@opencode-ai/schema/agent", Agent), ...namespaceTypes("Command", "@opencode-ai/schema/command", Command), + ...namespaceTypes("Config", "@opencode-ai/schema/config", Config), ...namespaceTypes("Credential", "@opencode-ai/schema/credential", Credential), ...namespaceTypes("Event", "@opencode-ai/schema/event", Event), ...namespaceTypes("EventLog", "@opencode-ai/schema/event-log", EventLog), diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index d45c50e8171..9af36aaff69 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -38,6 +38,7 @@ import type { ProjectCopy } from "@opencode-ai/schema/project-copy" import type { Vcs } from "@opencode-ai/schema/vcs" import type { FileDiff } from "@opencode-ai/schema/file-diff" import type { WebSearch } from "@opencode-ai/schema/websearch" +import type { Config } from "@opencode-ai/schema/config" export type Endpoint0_0Output = { readonly healthy: true; readonly version: string; readonly pid: number } export type HealthGetOperation = () => Effect.Effect @@ -1595,6 +1596,16 @@ export interface WebsearchApi { readonly query: WebsearchQueryOperation } +export type Endpoint29_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint29_0Output = ReadonlyArray +export type ConfigGetOperation = (input?: Endpoint29_0Input) => Effect.Effect + +export interface ConfigApi { + readonly get: ConfigGetOperation +} + export interface AppApi { readonly health: HealthApi readonly server: ServerApi @@ -1625,4 +1636,5 @@ export interface AppApi { readonly debug: DebugApi readonly migration: MigrationApi readonly websearch: WebsearchApi + readonly config: ConfigApi } diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index dc2071a54d6..55a50933cd9 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -220,6 +220,8 @@ import type { Endpoint28_0Output, Endpoint28_1Input, Endpoint28_1Output, + Endpoint29_0Input, + Endpoint29_0Output, } from "../api/api.js" import { ClientError } from "./client-error" @@ -1241,6 +1243,13 @@ const adaptGroup28 = (raw: RawClient["server.websearch"]) => ({ query: Endpoint28_1(raw), }) +const Endpoint29_0 = (raw: RawClient["server.config"]) => (input?: Endpoint29_0Input) => + preserveEffect()( + raw["config.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const adaptGroup29 = (raw: RawClient["server.config"]) => ({ get: Endpoint29_0(raw) }) + const adaptClient = (raw: RawClient) => ({ health: adaptGroup0(raw["server.health"]), server: adaptGroup1(raw["server.server"]), @@ -1271,6 +1280,7 @@ const adaptClient = (raw: RawClient) => ({ debug: adaptGroup26(raw["server.debug"]), migration: adaptGroup27(raw["server.migration"]), websearch: adaptGroup28(raw["server.websearch"]), + config: adaptGroup29(raw["server.config"]), }) export const make = (options?: { readonly baseUrl?: URL | string }) => diff --git a/packages/client/src/effect/index.ts b/packages/client/src/effect/index.ts index 500352062f2..7ae5c8ca43d 100644 --- a/packages/client/src/effect/index.ts +++ b/packages/client/src/effect/index.ts @@ -8,6 +8,7 @@ export type { AppApi, CatalogApi, CommandApi, + ConfigApi, EventApi, IntegrationApi, ModelApi, @@ -20,6 +21,7 @@ export type { } from "./api.js" export { Agent } from "@opencode-ai/schema/agent" export { Command } from "@opencode-ai/schema/command" +export { Config } from "@opencode-ai/schema/config" export { Credential } from "@opencode-ai/schema/credential" export { Event } from "@opencode-ai/schema/event" export { EventLog } from "@opencode-ai/schema/event-log" diff --git a/packages/client/src/promise/api.ts b/packages/client/src/promise/api.ts index 9614fef04c2..c682ad16f6e 100644 --- a/packages/client/src/promise/api.ts +++ b/packages/client/src/promise/api.ts @@ -2,6 +2,7 @@ type Client = ReturnType export type AgentApi = Client["agent"] export type CommandApi = Client["command"] +export type ConfigApi = Client["config"] export type EventApi = Client["event"] export type IntegrationApi = Client["integration"] export type ModelApi = Client["model"] diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 42d0440fc01..16147d51e76 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -216,6 +216,8 @@ import type { WebsearchProvidersOutput, WebsearchQueryInput, WebsearchQueryOutput, + ConfigGetInput, + ConfigGetOutput, } from "./types" import { ClientError } from "./client-error" @@ -1811,6 +1813,20 @@ export function make(options: ClientOptions) { requestOptions, ), }, + config: { + get: (input?: ConfigGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/config`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), + }, } } diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 010fb2dc218..3bb88bdaafb 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -1698,6 +1698,180 @@ export type AgentInfo = { permissions: PermissionRuleset } +export type ConfigEntry = + | { + type: "document" + path?: string | null + info: { + $schema?: string | null + shell?: string | null + model?: string | { providerID: string; model: string; variant?: string | null } | null + default_agent?: string | null + autoupdate?: boolean | "notify" | null + share?: "manual" | "auto" | "disabled" | null + enterprise?: { url?: string | null } | null + username?: string | null + permissions?: PermissionRuleset | null + agents?: { + [x: string]: { + model?: string | { providerID: string; model: string; variant?: string | null } | null + request?: { headers?: { [x: string]: string } | null; body?: { [x: string]: JsonValue } | null } | null + system?: string | null + description?: string | null + mode?: "subagent" | "primary" | "all" | null + hidden?: boolean | null + color?: string | null + steps?: number | null + disabled?: boolean | null + permissions?: PermissionRuleset | null + } + } | null + snapshots?: boolean | null + watcher?: { ignore?: Array | null } | null + formatter?: + | boolean + | { + [x: string]: { + disabled?: boolean | null + command?: Array | null + environment?: { [x: string]: string } | null + extensions?: Array | null + } + } + | null + lsp?: + | boolean + | { + [x: string]: + | { disabled: true } + | { + command: Array + extensions?: Array | null + disabled?: boolean | null + env?: { [x: string]: string } | null + initialization?: { [x: string]: JsonValue } | null + } + } + | null + media?: { + image?: { + auto_resize?: boolean | null + max_width?: number | null + max_height?: number | null + max_base64_bytes?: number | null + } | null + } | null + tool_output?: { max_lines?: number | null; max_bytes?: number | null } | null + mcp?: { + timeout?: { startup?: number | null; catalog?: number | null; execution?: number | null } | null + servers?: { + [x: string]: + | { + type: "local" + command: Array + cwd?: string | null + environment?: { [x: string]: string } | null + disabled?: boolean | null + codemode?: boolean | null + timeout?: { startup?: number | null; catalog?: number | null; execution?: number | null } | null + } + | { + type: "remote" + url: string + headers?: { [x: string]: string } | null + oauth?: + | { + client_id?: string | null + client_secret?: string | null + scope?: string | null + callback_port?: number | null + redirect_uri?: string | null + } + | false + | null + disabled?: boolean | null + codemode?: boolean | null + timeout?: { startup?: number | null; catalog?: number | null; execution?: number | null } | null + } + } | null + } | null + compaction?: { auto?: boolean | null; keep?: { tokens?: number | null } | null; buffer?: number | null } | null + skills?: Array | null + commands?: { + [x: string]: { + template: string + description?: string | null + agent?: string | null + model?: string | { providerID: string; model: string; variant?: string | null } | null + subtask?: boolean | null + } + } | null + instructions?: Array | null + references?: { + [x: string]: + | string + | { repository: string; branch?: string | null; description?: string | null; hidden?: boolean | null } + | { path: string; description?: string | null; hidden?: boolean | null } + } | null + websearch?: { provider: string } | null + plugins?: Array | null + warming?: boolean | { prompt?: string | null; interval?: string | null; duration?: string | null } | null + providers?: { + [x: string]: { + name?: string | null + env?: Array | null + package?: string | null + settings?: { [x: string]: JsonValue } | null + headers?: { [x: string]: string } | null + body?: { [x: string]: JsonValue } | null + models?: { + [x: string]: { + modelID?: string | null + family?: string | null + name?: string | null + compatibility?: ModelCompatibility | null + package?: string | null + settings?: { [x: string]: JsonValue } | null + headers?: { [x: string]: string } | null + body?: { [x: string]: JsonValue } | null + capabilities?: ModelCapabilities | null + variants?: Array<{ + id: string + settings?: { [x: string]: JsonValue } | null + headers?: { [x: string]: string } | null + body?: { [x: string]: JsonValue } | null + }> | null + cost?: + | { + tier?: { type: "context"; size: number } | null + input: MoneyUSDPerMillionTokens + output: MoneyUSDPerMillionTokens + cache?: { read?: MoneyUSDPerMillionTokens | null; write?: MoneyUSDPerMillionTokens | null } | null + } + | Array<{ + tier?: { type: "context"; size: number } | null + input: MoneyUSDPerMillionTokens + output: MoneyUSDPerMillionTokens + cache?: { read?: MoneyUSDPerMillionTokens | null; write?: MoneyUSDPerMillionTokens | null } | null + }> + | null + disabled?: boolean | null + limit?: { context?: number | null; input?: number | null; output?: number | null } | null + } + } | null + } + } | null + experimental?: { + subagent_depth?: number | null + policies?: Array<{ action: "provider.use"; resource: string; effect: "allow" | "deny" }> | null + } | null + } + } + | { type: "directory"; path: string } + | { type: "file"; path: string } + | { type: "agents"; path: string } + | { type: "claude"; path: string } + export type SessionsResponse = { data: Array; cursor: { previous?: string | null; next?: string | null } } export type SessionPendingUser = { @@ -4593,3 +4767,11 @@ export type WebsearchQueryOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } } data: { providerID: string; results: Array } } + +export type ConfigGetInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type ConfigGetOutput = Array diff --git a/packages/client/src/promise/index.ts b/packages/client/src/promise/index.ts index dd64831e013..a15ba0643e0 100644 --- a/packages/client/src/promise/index.ts +++ b/packages/client/src/promise/index.ts @@ -3,6 +3,7 @@ export type { AgentApi, CatalogApi, CommandApi, + ConfigApi, EventApi, IntegrationApi, ModelApi, diff --git a/packages/client/test/contract-identity.test.ts b/packages/client/test/contract-identity.test.ts index ace9d86a185..257f5124f5b 100644 --- a/packages/client/test/contract-identity.test.ts +++ b/packages/client/test/contract-identity.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" import { Schema } from "effect" import { Agent } from "@opencode-ai/schema/agent" +import { Config } from "@opencode-ai/schema/config" import { Model } from "@opencode-ai/schema/model" import { Prompt } from "@opencode-ai/schema/prompt" import { Session } from "@opencode-ai/schema/session" @@ -10,6 +11,7 @@ const Client = await import("../src/effect") test("effect entrypoint exposes canonical Schema contracts", () => { expect(Client.Agent).toBe(Agent) + expect(Client.Config).toBe(Config) expect(Client.Model).toBe(Model) expect(Client.Session).toBe(Session) }) diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index 450ebec5c7f..14a495cb543 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -33,6 +33,7 @@ test("exposes every standard HTTP API group", () => { "vcs", "debug", "websearch", + "config", ]) expect(Object.keys(client.debug)).toEqual(["location"]) expect(Object.keys(client.debug.location)).toEqual(["list", "evict"]) @@ -50,6 +51,34 @@ test("exposes every standard HTTP API group", () => { expect(Object.keys(client.project)).toEqual(["list", "current", "directories"]) }) +test("config.get returns ordered config entries for a location", async () => { + let request: Request | undefined + const entries = [ + { + type: "document" as const, + path: "/tmp/project/opencode.json", + info: { + permissions: [ + { action: "shell", resource: "*", effect: "ask" as const }, + { action: "shell", resource: "git status", effect: "allow" as const }, + ], + }, + }, + { type: "file" as const, path: "/tmp/project/opencode.json" }, + ] + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input) => { + request = input instanceof Request ? input : new Request(input) + return Response.json(entries) + }, + }) + + expect(await client.config.get({ location: { directory: "/tmp/project" } })).toEqual(entries) + expect(request?.method).toBe("GET") + expect(request?.url).toBe("http://localhost:3000/api/config?location%5Bdirectory%5D=%2Ftmp%2Fproject") +}) + test("websearch.query uses the public HTTP contract", async () => { let request: Request | undefined const client = OpenCode.make({ diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index de9e72b4792..fc9d8736123 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -5,8 +5,16 @@ import path from "path" import { isDeepStrictEqual } from "node:util" import { type ParseError, parse } from "jsonc-parser" import { Context, Effect, Layer, Option, PubSub, Ref, Schema, Semaphore, Stream } from "effect" -import { Permission } from "@opencode-ai/schema/permission" -import { Config as ConfigSchema } from "@opencode-ai/schema/config" +import { + AgentsDirectory, + ClaudeDirectory, + Directory, + Document, + File, + Info, + type Entry, + Event, +} from "@opencode-ai/schema/config" import { Integration } from "@opencode-ai/schema/integration" import { Credential } from "./credential" import { Bus } from "./bus" @@ -15,141 +23,11 @@ import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" import { Location } from "./location" import { AbsolutePath } from "./schema" -import { ConfigAgent } from "./config/agent" -import { ConfigMedia } from "./config/media" -import { ConfigCompaction } from "./config/compaction" -import { ConfigCommand } from "./config/command" -import { ConfigExperimental } from "./config/experimental" -import { ConfigFormatter } from "./config/formatter" -import { ConfigLSP } from "./config/lsp" -import { ConfigMCP } from "./config/mcp" -import { ConfigModel } from "./config/model" -import { ConfigPlugin } from "./config/plugin" -import { ConfigProvider } from "./config/provider" -import { ConfigReference } from "./config/reference" -import { ConfigWebSearch } from "./config/websearch" -import { ConfigToolOutput } from "./config/tool-output" import { ConfigVariable } from "./config/variable" -import { ConfigWatcher } from "./config/watcher" -import { ConfigWarming } from "./config/warming" import { ConfigV1 } from "./v1/config/config" import { ConfigMigrateV1 } from "./v1/config/migrate" import { WellKnown } from "./wellknown" -export class Info extends Schema.Class("Config.Info")({ - $schema: Schema.optional(Schema.String).annotate({ - description: "JSON schema reference for configuration validation", - }), - shell: Schema.String.pipe(Schema.optional).annotate({ - description: "Default shell to use for terminal and shell tool execution", - }), - model: ConfigModel.Selection.pipe(Schema.optional).annotate({ - description: "Default model to use when no session or agent model is selected", - }), - default_agent: Schema.String.pipe(Schema.optional).annotate({ - description: "Default primary agent to use when no session agent is selected", - }), - autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")]) - .pipe(Schema.optional) - .annotate({ - description: "Automatically update or notify when a new version is available", - }), - share: Schema.Literals(["manual", "auto", "disabled"]).pipe(Schema.optional).annotate({ - description: "Control whether sessions may be shared manually, automatically, or not at all", - }), - enterprise: Schema.Struct({ - url: Schema.String.pipe(Schema.optional), - }) - .pipe(Schema.optional) - .annotate({ - description: "Enterprise sharing service configuration", - }), - username: Schema.String.pipe(Schema.optional).annotate({ - description: "Username displayed in conversations and used for telemetry identity", - }), - permissions: Permission.Ruleset.pipe(Schema.optional).annotate({ - description: "Ordered tool permission rules applied to agent tool use", - }), - agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({ - description: "Named built-in agent overrides and custom agent definitions", - }), - snapshots: Schema.Boolean.pipe(Schema.optional).annotate({ - description: "Enable snapshots used for undo and revert behavior", - }), - watcher: ConfigWatcher.Info.pipe(Schema.optional).annotate({ - description: "Filesystem watcher configuration", - }), - formatter: ConfigFormatter.Info.pipe(Schema.optional).annotate({ - description: "Enable built-in formatters or configure formatter overrides", - }), - lsp: ConfigLSP.Info.pipe(Schema.optional).annotate({ - description: "Enable built-in language servers or configure server overrides", - }), - media: ConfigMedia.Info.pipe(Schema.optional).annotate({ - description: "Media processing configuration", - }), - tool_output: ConfigToolOutput.Info.pipe(Schema.optional).annotate({ - description: "Tool output truncation thresholds", - }), - mcp: ConfigMCP.Info.pipe(Schema.optional).annotate({ - description: "MCP server configuration", - }), - compaction: ConfigCompaction.Info.pipe(Schema.optional).annotate({ - description: "Conversation compaction behavior", - }), - skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ - description: "Additional paths or URLs to discover skills from", - }), - commands: Schema.Record(Schema.String, ConfigCommand.Info).pipe(Schema.optional).annotate({ - description: "Named slash command definitions", - }), - instructions: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ - description: "Additional paths or URLs supplying ambient instructions", - }), - references: ConfigReference.Info.pipe(Schema.optional).annotate({ - description: "Named local directories or Git repositories available as external context", - }), - websearch: ConfigWebSearch.Info.pipe(Schema.optional).annotate({ - description: "Web search provider selection", - }), - plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({ - description: "Ordered plugin enablement directives and external package declarations", - }), - warming: ConfigWarming.Warming.pipe(Schema.optional).annotate({ - description: "Keep recently active sessions warm with transient model requests (default: false)", - }), - providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional), - experimental: ConfigExperimental.Info.pipe(Schema.optional), -}) {} - -export class Document extends Schema.Class("Config.Document")({ - type: Schema.Literal("document"), - path: Schema.String.pipe(Schema.optional), - info: Info, -}) {} - -export class Directory extends Schema.Class("Config.Directory")({ - type: Schema.Literal("directory"), - path: AbsolutePath, -}) {} - -export class File extends Schema.Class("Config.File")({ - type: Schema.Literal("file"), - path: AbsolutePath, -}) {} - -export class AgentsDirectory extends Schema.Class("Config.AgentsDirectory")({ - type: Schema.Literal("agents"), - path: AbsolutePath, -}) {} - -export class ClaudeDirectory extends Schema.Class("Config.ClaudeDirectory")({ - type: Schema.Literal("claude"), - path: AbsolutePath, -}) {} - -export type Entry = Document | Directory | File | AgentsDirectory | ClaudeDirectory - export function latest(entries: readonly Entry[], key: K): Info[K] | undefined { return entries .filter((entry): entry is Document => entry.type === "document") @@ -296,21 +174,17 @@ export const layer = (options?: Options) => Layer.effect( // We load certain files from a few other folders in the ecosystem const claude = [ - ...((yield* fs.isDir(globalClaudeDirectory)) - ? [new ClaudeDirectory({ type: "claude", path: globalClaudeDirectory })] - : []), - ...discovered - .filter((item) => path.basename(item) === ".claude") - .map((directory) => new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(directory) })), - ] + ...new Set([ + ...((yield* fs.isDir(globalClaudeDirectory)) ? [globalClaudeDirectory] : []), + ...discovered.filter((item) => path.basename(item) === ".claude"), + ]), + ].map((directory) => new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(directory) })) const agents = [ - ...((yield* fs.isDir(globalAgentsDirectory)) - ? [new AgentsDirectory({ type: "agents", path: globalAgentsDirectory })] - : []), - ...discovered - .filter((item) => path.basename(item) === ".agents") - .map((directory) => new AgentsDirectory({ type: "agents", path: AbsolutePath.make(directory) })), - ] + ...new Set([ + ...((yield* fs.isDir(globalAgentsDirectory)) ? [globalAgentsDirectory] : []), + ...discovered.filter((item) => path.basename(item) === ".agents"), + ]), + ].map((directory) => new AgentsDirectory({ type: "agents", path: AbsolutePath.make(directory) })) const directories = [ globalDirectory, @@ -408,7 +282,7 @@ export const layer = (options?: Options) => Layer.effect( if (isDeepStrictEqual(configs, next)) return configs = next yield* reconcile(next) - yield* bus.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(Event.Updated, {}) }), ), ) diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index b8a689b160b..4e4d6ff7ca4 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -1,11 +1,12 @@ export * as ConfigAgentPlugin from "./agent" import { define } from "@opencode-ai/plugin/effect/plugin" +import { Document, Info, type Entry } from "@opencode-ai/schema/config" +import { ConfigAgent } from "@opencode-ai/schema/config/agent" import path from "path" import { Effect, Option, Schema, Stream } from "effect" import { Agent } from "../../agent" import { Config } from "../../config" -import { ConfigAgent } from "../agent" import { ConfigMarkdown } from "../markdown" import { FSUtil } from "@opencode-ai/util/fs-util" import { ConfigAgentV1 } from "../../v1/config/agent" @@ -24,7 +25,7 @@ const legacySources = [ const sourceDirectories = ["agent", "agents", "mode", "modes"] as const const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info) const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info) -const decodeConfig = Schema.decodeUnknownOption(Config.Info) +const decodeConfig = Schema.decodeUnknownOption(Info) type PathAction = | LocationMutation.ExternalDirectoryAuthorization["action"] | typeof ReadTool.name @@ -63,13 +64,13 @@ export const Plugin = define({ ), ).pipe( Effect.map((documents) => - documents.filter((document): document is Config.Document => document !== undefined), + documents.filter((document): document is Document => document !== undefined), ), ) }) }).pipe(Effect.map((documents) => documents.flat())) }) - const loaded = { documents: [] as Config.Document[] } + const loaded = { documents: [] as Document[] } const reload = load().pipe( Effect.tap((documents) => Effect.sync(() => (loaded.documents = documents))), Effect.andThen(ctx.agent.reload()), @@ -139,7 +140,7 @@ export const Plugin = define({ // Matches anything at or under /{agent,agents,mode,modes}. No file-suffix // check: directory-level events such as renames carry no per-file paths. -function isAgentSource(entries: Config.Entry[], file: string) { +function isAgentSource(entries: Entry[], file: string) { return entries.some( (entry) => entry.type === "directory" && @@ -208,5 +209,5 @@ function decode(file: { directory: string; filepath: string; primary: boolean }, }), ) if (!info) return - return new Config.Document({ type: "document", path: file.filepath, info }) + return new Document({ type: "document", path: file.filepath, info }) } diff --git a/packages/core/src/config/plugin/command.ts b/packages/core/src/config/plugin/command.ts index f035481fae0..360f5c477ac 100644 --- a/packages/core/src/config/plugin/command.ts +++ b/packages/core/src/config/plugin/command.ts @@ -1,12 +1,13 @@ export * as ConfigCommandPlugin from "./command" import { define } from "@opencode-ai/plugin/effect/plugin" +import { Info, type Entry } from "@opencode-ai/schema/config" +import { ConfigCommand } from "@opencode-ai/schema/config/command" import path from "path" import { Effect, Option, Schema, Stream } from "effect" import { Command } from "../../command" import { Config } from "../../config" import { FSUtil } from "@opencode-ai/util/fs-util" -import { ConfigCommand } from "../command" import { ConfigMarkdown } from "../markdown" const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info) @@ -27,7 +28,7 @@ export const Plugin = define({ ) }).pipe(Effect.map((documents) => documents.flat())) }) - const loaded = { documents: [] as { commands: Config.Info["commands"] }[] } + const loaded = { documents: [] as { commands: Info["commands"] }[] } const reload = load().pipe( Effect.tap((documents) => Effect.sync(() => (loaded.documents = documents))), Effect.andThen(ctx.command.reload()), @@ -75,7 +76,7 @@ const sourceDirectories = ["command", "commands"] as const // Matches anything at or under /{command,commands}. No file-suffix check: // directory-level events such as renames carry no per-file paths. -function isCommandSource(entries: Config.Entry[], file: string) { +function isCommandSource(entries: Entry[], file: string) { return entries.some( (entry) => entry.type === "directory" && diff --git a/packages/core/src/config/plugin/policy.ts b/packages/core/src/config/plugin/policy.ts index 97bacc51163..dac5a69d73f 100644 --- a/packages/core/src/config/plugin/policy.ts +++ b/packages/core/src/config/plugin/policy.ts @@ -1,6 +1,7 @@ export * as ConfigPolicyPlugin from "./policy" import { define } from "@opencode-ai/plugin/effect/plugin" +import { Document } from "@opencode-ai/schema/config" import { Effect, Stream } from "effect" import { Config } from "../../config" import { Wildcard } from "../../util/wildcard" @@ -13,7 +14,7 @@ export const Plugin = define({ yield* ctx.catalog.transform((catalog) => { // User-global policy takes priority over policy authored by a repository. const policies = loaded.entries - .filter((entry): entry is Config.Document => entry.type === "document") + .filter((entry): entry is Document => entry.type === "document") .toReversed() .flatMap((entry) => entry.info.experimental?.policies ?? []) for (const record of catalog.provider.list()) { diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index 5593c69b8fe..aa5f4fa7d59 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -1,6 +1,7 @@ export * as ConfigProviderPlugin from "./provider" import { define } from "@opencode-ai/plugin/effect/plugin" +import { Document, type Entry } from "@opencode-ai/schema/config" import { Money } from "@opencode-ai/schema/money" import { Effect, Stream } from "effect" import { Config } from "../../config" @@ -107,8 +108,8 @@ export const Plugin = define({ }), }) -function configuredProviders(entries: readonly Config.Entry[]) { +function configuredProviders(entries: readonly Entry[]) { return entries - .filter((entry): entry is Config.Document => entry.type === "document") + .filter((entry): entry is Document => entry.type === "document") .flatMap((file) => Object.entries(file.info.providers ?? {})) } diff --git a/packages/core/src/config/plugin/reference.ts b/packages/core/src/config/plugin/reference.ts index c0b62e00e75..145897d4a41 100644 --- a/packages/core/src/config/plugin/reference.ts +++ b/packages/core/src/config/plugin/reference.ts @@ -1,10 +1,11 @@ export * as ConfigReferencePlugin from "./reference" import { define } from "@opencode-ai/plugin/effect/plugin" +import { Document } from "@opencode-ai/schema/config" +import { ConfigReference } from "@opencode-ai/schema/config/reference" import path from "path" import { Effect, Stream } from "effect" import { Config } from "../../config" -import { ConfigReference } from "../reference" import { Reference } from "../../reference" import { AbsolutePath } from "../../schema" import { Global } from "@opencode-ai/util/global" @@ -19,7 +20,7 @@ export const Plugin = define({ const loaded = { entries: yield* config.entries() } yield* ctx.reference.transform((draft) => { const entries = new Map() - for (const doc of loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")) { + for (const doc of loaded.entries.filter((entry): entry is Document => entry.type === "document")) { const directory = doc.path ? path.dirname(doc.path) : location.directory for (const [name, entry] of Object.entries(doc.info.references ?? {})) { if (!validAlias(name)) continue diff --git a/packages/core/src/filesystem/location-watcher.ts b/packages/core/src/filesystem/location-watcher.ts index 108517a5661..a451fc266e4 100644 --- a/packages/core/src/filesystem/location-watcher.ts +++ b/packages/core/src/filesystem/location-watcher.ts @@ -3,6 +3,7 @@ export * as LocationWatcher from "./location-watcher" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Effect, Layer, Stream } from "effect" import { FileSystem } from "@opencode-ai/schema/filesystem" +import { Document } from "@opencode-ai/schema/config" import path from "path" import { Config } from "../config" import { Bus } from "../bus" @@ -41,7 +42,7 @@ const layer = Layer.effect( yield* Effect.gen(function* () { const config = (yield* configService.entries()) - .filter((entry): entry is Config.Document => entry.type === "document") + .filter((entry): entry is Document => entry.type === "document") .flatMap((item) => item.info.watcher?.ignore ?? []) const home = Protected.isHome(location.directory) diff --git a/packages/core/src/mcp/client.ts b/packages/core/src/mcp/client.ts index 630ff88b980..b8f34b1996d 100644 --- a/packages/core/src/mcp/client.ts +++ b/packages/core/src/mcp/client.ts @@ -29,7 +29,7 @@ import { ToolSchema, } from "@modelcontextprotocol/sdk/types.js" import { Cause, Effect, Exit, Schema } from "effect" -import { ConfigMCP } from "../config/mcp" +import { ConfigMCP } from "@opencode-ai/schema/config/mcp" const DEFAULT_STARTUP_TIMEOUT = 30_000 const DEFAULT_CATALOG_TIMEOUT = 30_000 diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts index ef8aa09941f..8d853f06737 100644 --- a/packages/core/src/mcp/index.ts +++ b/packages/core/src/mcp/index.ts @@ -3,11 +3,12 @@ export * as MCP from "./index" import { Mcp } from "@opencode-ai/schema/mcp" import { McpEvent } from "@opencode-ai/schema/mcp-event" import { Command } from "@opencode-ai/schema/command" +import { Document } from "@opencode-ai/schema/config" +import { ConfigMCP } from "@opencode-ai/schema/config/mcp" import { createHash } from "node:crypto" import { Cause, Context, Deferred, Effect, Exit, FiberSet, Layer, Schema, Scope, Stream } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Config } from "../config" -import { ConfigMCP } from "../config/mcp" import { Credential } from "../credential" import { Bus } from "../bus" import { Form } from "../form" @@ -178,7 +179,7 @@ export const layer = (options?: Options) => Layer.effect( const fork = yield* FiberSet.makeRuntime() yield* Effect.addFinalizer((exit) => Scope.close(root, exit)) - const documents = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document") + const documents = (yield* config.entries()).filter((entry): entry is Document => entry.type === "document") // Global MCP timeout defaults, later config files overriding earlier ones. const timeout = Object.assign( {}, diff --git a/packages/core/src/mcp/oauth.ts b/packages/core/src/mcp/oauth.ts index fc4752c7af8..1015e1bd403 100644 --- a/packages/core/src/mcp/oauth.ts +++ b/packages/core/src/mcp/oauth.ts @@ -5,7 +5,7 @@ import type { OAuthClientInformationMixed, OAuthTokens } from "@modelcontextprot import { createServer } from "node:http" import { Deferred, Effect } from "effect" import { Credential } from "@opencode-ai/schema/credential" -import { ConfigMCP } from "../config/mcp" +import { ConfigMCP } from "@opencode-ai/schema/config/mcp" import { OauthCallbackPage } from "../oauth/page" import type { Integration } from "../integration" diff --git a/packages/core/src/plugin/supervisor.ts b/packages/core/src/plugin/supervisor.ts index 160528b8b8d..5ccbec0243e 100644 --- a/packages/core/src/plugin/supervisor.ts +++ b/packages/core/src/plugin/supervisor.ts @@ -1,7 +1,8 @@ export * as PluginSupervisor from "./supervisor" import type { Plugin as PluginDefinition } from "@opencode-ai/plugin/effect/plugin" -import { Event } from "@opencode-ai/schema/config" +import { Directory, Document, Event, type Entry } from "@opencode-ai/schema/config" +import { ConfigPlugin } from "@opencode-ai/schema/config/plugin" import { Context, Deferred, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" import path from "path" import { fileURLToPath, pathToFileURL } from "url" @@ -9,7 +10,6 @@ import { Agent } from "../agent" import { Catalog } from "../catalog" import { Command } from "../command" import { Config } from "../config" -import { ConfigPlugin } from "../config/plugin" import { Credential } from "../credential" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { httpClient } from "@opencode-ai/util/effect/app-node-platform" @@ -83,15 +83,15 @@ function parse(input: ConfigPlugin.Plugin): Operation { return { type: "remove", target: input.slice(1) } } -const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Config.Entry[]) { +const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Entry[]) { const fs = yield* FSUtil.Service const location = yield* Location.Service const discovered = yield* Effect.forEach( - entries.filter((entry): entry is Config.Directory => entry.type === "directory"), + entries.filter((entry): entry is Directory => entry.type === "directory"), (entry) => discoverDirectory(fs, entry.path), ).pipe(Effect.map((items) => items.flat())) const configured = entries - .filter((entry): entry is Config.Document => entry.type === "document") + .filter((entry): entry is Document => entry.type === "document") .flatMap((entry) => (entry.info.plugins ?? []).map(parse).map((operation) => { if (operation.type === "remove") return operation @@ -208,7 +208,7 @@ function discoverDirectory(fs: FSUtil.Interface, directory: string) { const sourceDirectories = ["plugin", "plugins"] as const -function isPluginSource(entries: readonly Config.Entry[], file: string) { +function isPluginSource(entries: readonly Entry[], file: string) { return entries.some( (entry) => entry.type === "directory" && @@ -243,7 +243,7 @@ const layer = Layer.effect( const configuredChanges = yield* PubSub.unbounded() const watched = new Set() const watchConfiguredSources = Effect.fn("PluginSupervisor.watchConfiguredSources")(function* ( - entries: readonly Config.Entry[], + entries: readonly Entry[], operations: readonly Operation[], ) { for (const operation of operations) { diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index d523f38abf4..a6c1e4d4587 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -2,6 +2,7 @@ export * as SessionCompaction from "./compaction" import { LLM, LLMClient, AIError, LLMEvent, Message, type LLMRequest, type LanguageModel } from "@opencode-ai/ai" import { SessionError } from "@opencode-ai/schema/session-error" +import { Document, type Entry } from "@opencode-ai/schema/config" import { Context, Effect, Layer, Stream } from "effect" import { Config } from "../config" import { Bus } from "../bus" @@ -148,9 +149,9 @@ const serialize = (message: SessionMessage.Info) => { return "" } -const settings = (documents: readonly Config.Entry[]) => { +const settings = (documents: readonly Entry[]) => { const configured = documents - .filter((entry): entry is Config.Document => entry.type === "document") + .filter((entry): entry is Document => entry.type === "document") .flatMap((entry) => (entry.info.compaction ? [entry.info.compaction] : [])) return { auto: configured.findLast((value) => value.auto !== undefined)?.auto ?? true, diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index e3ecf7324f7..5e15f9dfdc1 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -1,8 +1,8 @@ export * as ConfigV1 from "./config" import { Schema } from "effect" +import { ConfigReference } from "@opencode-ai/schema/config/reference" import { NonNegativeInt, PositiveInt, type DeepMutable } from "../../schema" -import { ConfigReference } from "../../config/reference" import { ConfigAgentV1 } from "./agent" import { ConfigAttachmentV1 } from "./attachment" import { ConfigCommandV1 } from "./command" diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index 2cb8f0e9ccd..0060d73bd0e 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -5,6 +5,7 @@ import { Effect, Fiber, Schema, Stream } from "effect" import { Agent } from "@opencode-ai/core/agent" import { Bus } from "@opencode-ai/core/bus" import { Config } from "@opencode-ai/core/config" +import { Directory, Document, Info } from "@opencode-ai/schema/config" import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" @@ -19,7 +20,7 @@ import { testEffect } from "../lib/effect" import { agentHost, host } from "../plugin/host" const it = testEffect(AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, FSUtil.node, Global.node]))) -const decode = Schema.decodeUnknownSync(Config.Info) +const decode = Schema.decodeUnknownSync(Info) const defaultPermissions = (global: Global.Interface): Permission.Ruleset => [ ...Agent.Info.default(Agent.ID.make("test")).permissions, { action: "external_directory", resource: path.join(global.data, "shell", "*", "*"), effect: "allow" }, @@ -71,7 +72,7 @@ describe("ConfigAgentPlugin.Plugin", () => { ) const entries = [ - new Config.Document({ + new Document({ type: "document", info: decode({ permissions: [{ action: "bash", resource: "*", effect: "ask" }], @@ -92,7 +93,7 @@ describe("ConfigAgentPlugin.Plugin", () => { }, }), }), - new Config.Document({ + new Document({ type: "document", info: decode({ permissions: [{ action: "read", resource: "*", effect: "allow" }], @@ -153,7 +154,7 @@ describe("ConfigAgentPlugin.Plugin", () => { Effect.gen(function* () { const agents = yield* Agent.Service const entries = [ - new Config.Document({ + new Document({ type: "document", info: decode({ agents: { @@ -173,7 +174,7 @@ describe("ConfigAgentPlugin.Plugin", () => { }, }), }), - new Config.Document({ + new Document({ type: "document", info: decode({ agents: { @@ -218,7 +219,7 @@ describe("ConfigAgentPlugin.Plugin", () => { yield* agents.transform((editor) => editor.update(build, () => {})) const entries = [ - new Config.Document({ + new Document({ type: "document", info: decode({ agents: { build: { disabled: true } } }), }), @@ -276,7 +277,7 @@ Use native v2 fields.`, const agents = yield* Agent.Service const global = yield* Global.Service const entries = [ - new Config.Document({ + new Document({ type: "document", info: decode({ agents: { reviewer: { description: "JSON description" } } }), }), @@ -425,7 +426,7 @@ Use native v2 fields.`, }) function directoryEntry(directory: string) { - return new Config.Directory({ type: "directory", path: AbsolutePath.make(directory) }) + return new Directory({ type: "directory", path: AbsolutePath.make(directory) }) } function sourceCases() { @@ -522,7 +523,7 @@ function loadHomePermissions(home: string) { const build = Agent.ID.make("build") yield* agents.transform((editor) => editor.update(build, () => {})) const entries = [ - new Config.Document({ + new Document({ type: "document", info: decode( ConfigMigrateV1.migrate({ diff --git a/packages/core/test/config/command.test.ts b/packages/core/test/config/command.test.ts index 6aba385a4b3..c196f6bc327 100644 --- a/packages/core/test/config/command.test.ts +++ b/packages/core/test/config/command.test.ts @@ -3,7 +3,7 @@ import path from "path" import { describe, expect } from "bun:test" import { Deferred, Effect, Fiber, Layer, Option, PubSub, Schema, Stream } from "effect" import { advance, drain } from "../lib/clock" -import { Config as ConfigSchema } from "@opencode-ai/schema/config" +import { Directory, Document, Event, Info } from "@opencode-ai/schema/config" import { Command } from "@opencode-ai/core/command" import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" @@ -35,7 +35,7 @@ const it = testEffect( [Location.node, testLocationLayer], ]), ) -const decode = Schema.decodeUnknownSync(Config.Info) +const decode = Schema.decodeUnknownSync(Info) describe("ConfigCommandPlugin.Plugin", () => { it.live("loads inline and file-based commands in config order", () => @@ -63,7 +63,7 @@ Review files`, const command = yield* Command.Service const bus = yield* Bus.Service - const update = yield* bus.publish(ConfigSchema.Event.Updated, {}) + const update = yield* bus.publish(Event.Updated, {}) const updates = yield* PubSub.unbounded() yield* ConfigCommandPlugin.Plugin.effect( host({ @@ -77,11 +77,11 @@ Review files`, ).pipe( Effect.provide( Config.testLayer([ - new Config.Document({ + new Document({ type: "document", info: decode({ commands: { review: { template: "Inline review" } } }), }), - new Config.Directory({ type: "directory", path: AbsolutePath.make(tmp.path) }), + new Directory({ type: "directory", path: AbsolutePath.make(tmp.path) }), ]), ), ) @@ -333,7 +333,7 @@ function watchReady(config: Config.Interface, directory: string) { } function directoryEntry(directory: string) { - return new Config.Directory({ type: "directory", path: AbsolutePath.make(directory) }) + return new Directory({ type: "directory", path: AbsolutePath.make(directory) }) } function sourceCases() { diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index c43fc9d330d..8e10d5801a0 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -4,9 +4,9 @@ import { describe, expect } from "bun:test" import { Effect, Fiber, Layer, PubSub, Schema, Stream } from "effect" import { FastCheck } from "effect/testing" import { Config } from "@opencode-ai/core/config" -import { ConfigModel } from "@opencode-ai/core/config/model" -import { Config as ConfigSchema } from "@opencode-ai/schema/config" -import { ConfigProvider } from "@opencode-ai/core/config/provider" +import { AgentsDirectory, Directory, Document, Event, Info } from "@opencode-ai/schema/config" +import { ConfigModel } from "@opencode-ai/schema/config/model" +import { ConfigProvider } from "@opencode-ai/schema/config/provider" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { LayerNode } from "@opencode-ai/util/effect/layer-node" @@ -161,7 +161,7 @@ describe("Config", () => { const bus = yield* Bus.Service const watcher = yield* Watcher.Test const changed = yield* bus - .subscribe(ConfigSchema.Event.Updated) + .subscribe(Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.sleep("10 millis") @@ -234,14 +234,14 @@ describe("Config", () => { yield* Effect.sleep("10 millis") const removed = yield* bus - .subscribe(ConfigSchema.Event.Updated) + .subscribe(Event.Updated) .pipe(Stream.take(1), Stream.runDrain, Effect.forkScoped({ startImmediately: true })) yield* Effect.promise(() => fs.rm(file)) yield* Fiber.join(removed).pipe(Effect.timeout("5 seconds")) expect(Config.latest(yield* config.entries(), "shell")).toBeUndefined() const recreated = yield* bus - .subscribe(ConfigSchema.Event.Updated) + .subscribe(Event.Updated) .pipe(Stream.take(1), Stream.runDrain, Effect.forkScoped({ startImmediately: true })) yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ shell: "two" }))) yield* Fiber.join(recreated).pipe(Effect.timeout("5 seconds")) @@ -273,7 +273,7 @@ describe("Config", () => { const test = yield* Config.Test expect(yield* config.entries()).toEqual([]) - const entry = new Config.Document({ type: "document", info: new Config.Info({}) }) + const entry = new Document({ type: "document", info: new Info({}) }) yield* test.setEntries([entry]) expect(yield* config.entries()).toEqual([entry]) @@ -289,16 +289,16 @@ describe("Config", () => { it.effect("returns the latest defined scalar from priority-ordered documents", () => Effect.sync(() => { const entries = [ - new Config.Document({ + new Document({ type: "document", - info: new Config.Info({ model: selection("openrouter/openai/gpt-5") }), + info: new Info({ model: selection("openrouter/openai/gpt-5") }), }), - new Config.Directory({ type: "directory", path: AbsolutePath.make("/skills") }), - new Config.AgentsDirectory({ type: "agents", path: AbsolutePath.make("/agents") }), - new Config.Document({ type: "document", info: new Config.Info({}) }), - new Config.Document({ + new Directory({ type: "directory", path: AbsolutePath.make("/skills") }), + new AgentsDirectory({ type: "agents", path: AbsolutePath.make("/agents") }), + new Document({ type: "document", info: new Info({}) }), + new Document({ type: "document", - info: new Config.Info({ model: selection("openrouter/openai/gpt-5.5") }), + info: new Info({ model: selection("openrouter/openai/gpt-5.5") }), }), ] @@ -372,7 +372,7 @@ describe("Config", () => { const bus = yield* Bus.Service expect(Config.latest(yield* config.entries(), "shell")).toBe("secret") const updated = yield* bus - .subscribe(ConfigSchema.Event.Updated) + .subscribe(Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow key = "next" @@ -418,7 +418,7 @@ describe("Config", () => { Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(info), ), ) - Schema.decodeUnknownSync(Config.Info)(ConfigMigrateV1.migrate(parsed), { errors: "all" }) + Schema.decodeUnknownSync(Info)(ConfigMigrateV1.migrate(parsed), { errors: "all" }) }), { numRuns: 100 }, ) @@ -661,13 +661,45 @@ describe("Config", () => { const entries = yield* config.entries() expect(entries).toEqual([ - new Config.Directory({ type: "directory", path: AbsolutePath.make(path.join(tmp.path, "global")) }), + new Directory({ type: "directory", path: AbsolutePath.make(path.join(tmp.path, "global")) }), ]) }).pipe(Effect.provide(testLayer(tmp.path))), ), ), ) + it.live("deduplicates global ecosystem directories found during upward discovery", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + const global = path.join(tmp.path, "global") + const home = path.join(global, "home") + const project = path.join(home, "project") + yield* Effect.promise(() => + Promise.all([ + fs.mkdir(path.join(home, ".claude"), { recursive: true }), + fs.mkdir(path.join(home, ".agents"), { recursive: true }), + fs.mkdir(project, { recursive: true }), + ]), + ) + const entries = yield* Config.Service.use((config) => config.entries()).pipe( + Effect.provide(testLayer(project, global)), + ) + + expect(entries.filter((entry) => entry.type === "claude").map((entry) => entry.path)).toEqual([ + AbsolutePath.make(path.join(home, ".claude")), + ]) + expect(entries.filter((entry) => entry.type === "agents").map((entry) => entry.path)).toEqual([ + AbsolutePath.make(path.join(home, ".agents")), + ]) + }), + ), + ), + ) + it.live("does not watch ecosystem config roots", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), @@ -729,7 +761,7 @@ describe("Config", () => { expect(documents).toHaveLength(2) expect(documents.map((document) => document.type)).toEqual(["document", "document"]) expect(documents.map((document) => document.info.$schema)).toEqual(["base", "last"]) - expect(documents[0]).toBeInstanceOf(Config.Document) + expect(documents[0]).toBeInstanceOf(Document) expect(documents[0]?.path).toBe(path.join(tmp.path, "opencode.json")) expect(documents[1]?.info.providers?.last).toBeInstanceOf(ConfigProvider.Info) @@ -1177,7 +1209,7 @@ describe("Config", () => { const documents = (yield* config.entries()).filter((entry) => entry.type === "document") expect(documents).toHaveLength(1) - expect(documents[0]?.info).toBeInstanceOf(Config.Info) + expect(documents[0]?.info).toBeInstanceOf(Info) expect(documents[0]?.info.shell).toBe("/bin/zsh") expect(documents[0]?.info.default_agent).toBe("reviewer") expect(documents[0]?.info.snapshots).toBe(false) diff --git a/packages/core/test/config/model.test.ts b/packages/core/test/config/model.test.ts index 6ef8b2ac75f..940e9eeef47 100644 --- a/packages/core/test/config/model.test.ts +++ b/packages/core/test/config/model.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { ConfigModel } from "@opencode-ai/core/config/model" +import { ConfigModel } from "@opencode-ai/schema/config/model" import { Model } from "@opencode-ai/schema/model" import { Provider } from "@opencode-ai/schema/provider" import { Schema } from "effect" diff --git a/packages/core/test/config/policy.test.ts b/packages/core/test/config/policy.test.ts index e85b8271a9d..60afda1dac4 100644 --- a/packages/core/test/config/policy.test.ts +++ b/packages/core/test/config/policy.test.ts @@ -1,5 +1,5 @@ import { describe, expect } from "bun:test" -import { Config as ConfigSchema } from "@opencode-ai/schema/config" +import { Document, Event, Info, type Entry } from "@opencode-ai/schema/config" import { Catalog } from "@opencode-ai/core/catalog" import { Config } from "@opencode-ai/core/config" import { ConfigPolicyPlugin } from "@opencode-ai/core/config/plugin/policy" @@ -12,10 +12,10 @@ import { testEffect } from "../lib/effect" import { PluginTestLayer } from "../plugin/fixture" const it = testEffect(PluginTestLayer) -const decode = Schema.decodeUnknownSync(Config.Info) +const decode = Schema.decodeUnknownSync(Info) const policies = (...items: { effect: "allow" | "deny"; resource: string }[]) => - new Config.Document({ + new Document({ type: "document", info: decode({ experimental: { @@ -24,7 +24,7 @@ const policies = (...items: { effect: "allow" | "deny"; resource: string }[]) => }), }) -const addPlugin = Effect.fn(function* (entries: Config.Entry[]) { +const addPlugin = Effect.fn(function* (entries: Entry[]) { const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* ConfigPolicyPlugin.Plugin.effect(host).pipe(Effect.provide(Config.testLayer(entries))) @@ -78,7 +78,7 @@ describe("ConfigPolicyPlugin.Plugin", () => { expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined() yield* test.setEntries([policies({ effect: "allow", resource: "openai" })]) - yield* bus.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(Event.Updated, {}) yield* waitUntil(catalog.provider.get(Provider.ID.openai).pipe(Effect.map((provider) => provider !== undefined))) }).pipe(Effect.provide(Config.testLayer([policies({ effect: "deny", resource: "openai" })]))), ) diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index de10dbcd368..21680c856df 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -1,5 +1,6 @@ import { describe, expect } from "bun:test" import { Money } from "@opencode-ai/schema/money" +import { Document, Info, type Entry } from "@opencode-ai/schema/config" import { Effect, Schema, Stream } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Config } from "@opencode-ai/core/config" @@ -14,7 +15,7 @@ import { PluginTestLayer } from "../plugin/fixture" const it = testEffect(PluginTestLayer) -const addPlugin = Effect.fn(function* (entries: Config.Entry[]) { +const addPlugin = Effect.fn(function* (entries: Entry[]) { const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provide(Config.testLayer(entries))) @@ -46,7 +47,7 @@ function withEnv(vars: Record, effect: () = ) } -const decode = Schema.decodeUnknownSync(Config.Info) +const decode = Schema.decodeUnknownSync(Info) describe("ConfigProviderPlugin.Plugin", () => { it.effect("defaults custom models to agent capabilities", () => @@ -55,7 +56,7 @@ describe("ConfigProviderPlugin.Plugin", () => { const providerID = Provider.ID.make("custom") const modelID = Model.ID.make("chat") const entries = [ - new Config.Document({ + new Document({ type: "document", info: decode({ providers: { @@ -90,7 +91,7 @@ describe("ConfigProviderPlugin.Plugin", () => { }) }) const entries = [ - new Config.Document({ + new Document({ type: "document", info: decode({ providers: { @@ -129,7 +130,7 @@ describe("ConfigProviderPlugin.Plugin", () => { const providerID = Provider.ID.opencode const modelID = Model.ID.make("alpha-gpt-next") const entries = [ - new Config.Document({ + new Document({ type: "document", info: decode({ providers: { @@ -178,7 +179,7 @@ describe("ConfigProviderPlugin.Plugin", () => { const providerID = Provider.ID.opencode const modelID = Model.ID.make("alpha-gpt-next") const entries = [ - new Config.Document({ + new Document({ type: "document", info: decode({ providers: { @@ -189,7 +190,7 @@ describe("ConfigProviderPlugin.Plugin", () => { }, }), }), - new Config.Document({ + new Document({ type: "document", info: decode({ providers: { @@ -223,7 +224,7 @@ describe("ConfigProviderPlugin.Plugin", () => { const providerID = Provider.ID.make("custom") const modelID = Model.ID.make("chat") const entries = [ - new Config.Document({ + new Document({ type: "document", info: decode({ model: "custom/first", @@ -255,7 +256,7 @@ describe("ConfigProviderPlugin.Plugin", () => { }, }), }), - new Config.Document({ + new Document({ type: "document", info: decode({ model: "custom/default", @@ -289,7 +290,7 @@ describe("ConfigProviderPlugin.Plugin", () => { }, }), }), - new Config.Document({ + new Document({ type: "document", info: decode({ providers: { diff --git a/packages/core/test/config/reload.test.ts b/packages/core/test/config/reload.test.ts index 7c4a7cb10f6..85eb0791113 100644 --- a/packages/core/test/config/reload.test.ts +++ b/packages/core/test/config/reload.test.ts @@ -1,6 +1,6 @@ import path from "path" import { describe, expect } from "bun:test" -import { Config as ConfigSchema } from "@opencode-ai/schema/config" +import { Document, Event, Info } from "@opencode-ai/schema/config" import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { Command } from "@opencode-ai/core/command" @@ -22,7 +22,7 @@ import { testEffect } from "../lib/effect" import { PluginTestLayer } from "../plugin/fixture" const it = testEffect(PluginTestLayer) -const decode = Schema.decodeUnknownSync(Config.Info) +const decode = Schema.decodeUnknownSync(Info) const document = path.join(import.meta.dir, "opencode.json") describe("config plugin reloads", () => { @@ -54,7 +54,7 @@ describe("config plugin reloads", () => { yield* test.setEntries([config("second")]) yield* Effect.yieldNow - yield* bus.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(Event.Updated, {}) yield* waitUntil( Effect.gen(function* () { return ( @@ -83,7 +83,7 @@ describe("config plugin reloads", () => { }) function config(name: string) { - return new Config.Document({ + return new Document({ type: "document", path: document, info: decode({ diff --git a/packages/core/test/config/skill.test.ts b/packages/core/test/config/skill.test.ts index bf4264f3eb6..791eba80fa0 100644 --- a/packages/core/test/config/skill.test.ts +++ b/packages/core/test/config/skill.test.ts @@ -2,6 +2,7 @@ import path from "path" import { describe, expect } from "bun:test" import { Effect, Layer, Schema, Stream } from "effect" import { Config } from "@opencode-ai/core/config" +import { AgentsDirectory, ClaudeDirectory, Directory, Document, Info } from "@opencode-ai/schema/config" import { ConfigSkillPlugin } from "@opencode-ai/core/config/plugin/skill" import { Global } from "@opencode-ai/util/global" import { Location } from "@opencode-ai/core/location" @@ -12,7 +13,7 @@ import { testEffect } from "../lib/effect" import { host } from "../plugin/host" const it = testEffect(Layer.empty) -const decode = Schema.decodeUnknownSync(Config.Info) +const decode = Schema.decodeUnknownSync(Info) describe("ConfigSkillPlugin.Plugin", () => { it.effect("registers configured skill directories and URLs", () => @@ -43,10 +44,10 @@ describe("ConfigSkillPlugin.Plugin", () => { Effect.provideService(Location.Service, Location.Service.of(location({ directory }))), Effect.provide( Config.testLayer([ - new Config.ClaudeDirectory({ type: "claude", path: AbsolutePath.make("/repo/.claude") }), - new Config.AgentsDirectory({ type: "agents", path: AbsolutePath.make("/repo/.agents") }), - new Config.Directory({ type: "directory", path: AbsolutePath.make("/repo/.opencode") }), - new Config.Document({ + new ClaudeDirectory({ type: "claude", path: AbsolutePath.make("/repo/.claude") }), + new AgentsDirectory({ type: "agents", path: AbsolutePath.make("/repo/.agents") }), + new Directory({ type: "directory", path: AbsolutePath.make("/repo/.opencode") }), + new Document({ type: "document", info: decode({ skills: ["./skills", "~/shared-skills", "/opt/skills", "https://example.test/skills/"], diff --git a/packages/core/test/config/warming.test.ts b/packages/core/test/config/warming.test.ts index d0ecbf3a914..6b1ebfbce63 100644 --- a/packages/core/test/config/warming.test.ts +++ b/packages/core/test/config/warming.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test" import { Duration, Schema } from "effect" -import { Config } from "../../src/config" +import { Info } from "@opencode-ai/schema/config" -const decode = Schema.decodeUnknownSync(Config.Info) +const decode = Schema.decodeUnknownSync(Info) describe("config warming", () => { test("accepts boolean enablement", () => { diff --git a/packages/core/test/formatter.test.ts b/packages/core/test/formatter.test.ts index 2a837e6f0db..441ee4fdb6e 100644 --- a/packages/core/test/formatter.test.ts +++ b/packages/core/test/formatter.test.ts @@ -5,6 +5,7 @@ import { Effect, Layer, Schema, Stream } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AbsolutePath } from "@opencode-ai/core/schema" import { Npm } from "@opencode-ai/util/npm" +import { Document, Info } from "@opencode-ai/schema/config" import { Config } from "../src/config" import { Formatter } from "../src/formatter" import { Location } from "../src/location" @@ -13,16 +14,16 @@ import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" const it = testEffect(Layer.empty) -type ConfigInput = typeof Config.Info.Encoded +type ConfigInput = typeof Info.Encoded function formatterLayer(directory: string, configured?: ConfigInput["formatter"]) { const entries = configured === undefined ? [] : [ - new Config.Document({ + new Document({ type: "document", - info: Schema.decodeUnknownSync(Config.Info)({ formatter: configured }), + info: Schema.decodeUnknownSync(Info)({ formatter: configured }), }), ] return AppNodeBuilder.build(Formatter.node, [ diff --git a/packages/core/test/mcp.test.ts b/packages/core/test/mcp.test.ts index f98a380f5bf..846b8835e7a 100644 --- a/packages/core/test/mcp.test.ts +++ b/packages/core/test/mcp.test.ts @@ -12,7 +12,8 @@ import { ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js" -import { ConfigMCP } from "@opencode-ai/core/config/mcp" +import { Document, Info } from "@opencode-ai/schema/config" +import { ConfigMCP } from "@opencode-ai/schema/config/mcp" import { Config } from "@opencode-ai/core/config" import { Credential } from "@opencode-ai/core/credential" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -162,9 +163,9 @@ function resourceMcpLayer( Layer.provide( Layer.mergeAll( Config.testLayer([ - new Config.Document({ + new Document({ type: "document", - info: new Config.Info({ + info: new Info({ mcp: new ConfigMCP.Info({ servers: { resources: diff --git a/packages/core/test/pty/pty-session.test.ts b/packages/core/test/pty/pty-session.test.ts index 05f6dbc588f..e4f0bdf6e78 100644 --- a/packages/core/test/pty/pty-session.test.ts +++ b/packages/core/test/pty/pty-session.test.ts @@ -1,6 +1,7 @@ import { describe, expect } from "bun:test" import { Cause, Deferred, Effect, Exit, Layer, Queue } from "effect" import { Config } from "@opencode-ai/core/config" +import { Document, Info } from "@opencode-ai/schema/config" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Bus } from "@opencode-ai/core/bus" @@ -213,7 +214,7 @@ const configuredIt = testEffect( entries: () => Effect.succeed( configuredShell - ? [new Config.Document({ type: "document", info: new Config.Info({ shell: configuredShell }) })] + ? [new Document({ type: "document", info: new Info({ shell: configuredShell }) })] : [], ), }), diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 426a8cd4ffb..72d15f16b18 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -49,9 +49,10 @@ import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt" import { QuestionTool } from "@opencode-ai/core/tool/plugin/question" import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" -import { ConfigCompaction } from "@opencode-ai/core/config/compaction" +import { Document, Info } from "@opencode-ai/schema/config" +import { ConfigCompaction } from "@opencode-ai/schema/config/compaction" import { Tool } from "@opencode-ai/core/tool" -import type { Info } from "@opencode-ai/schema/tool" +import type { Info as ToolInfo } from "@opencode-ai/schema/tool" import { InstructionStateTable, SessionPendingTable, @@ -228,7 +229,7 @@ const permission = Layer.succeed( list: () => Effect.die("unused"), }), ) -const transformTools = (registry: Tool.Interface, tools: Readonly>, options?: Tool.Options) => +const transformTools = (registry: Tool.Interface, tools: Readonly>, options?: Tool.Options) => registry.transform((draft) => Object.entries(tools).forEach(([name, tool]) => draft.add({ ...tool, name, options: options ?? tool.options })), ) @@ -334,9 +335,9 @@ const referenceInstructions = Layer.mock(ReferenceInstructions.Service, { }) const mcpInstructions = Layer.mock(McpInstructions.Service, { load: () => Effect.succeed(Instructions.empty) }) const config = Config.testLayer([ - new Config.Document({ + new Document({ type: "document", - info: new Config.Info({ + info: new Info({ compaction: new ConfigCompaction.Info({ buffer: 3_000, keep: new ConfigCompaction.Keep({ tokens: 1_000 }), diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts index c9b549646e4..700909aa4c4 100644 --- a/packages/core/test/tool-read.test.ts +++ b/packages/core/test/tool-read.test.ts @@ -2,7 +2,8 @@ import { beforeEach, describe, expect } from "bun:test" import path from "path" import { Effect, Exit, Layer, PlatformError, Stream } from "effect" import { Config } from "@opencode-ai/core/config" -import { ConfigMedia } from "@opencode-ai/core/config/media" +import { Document, Info } from "@opencode-ai/schema/config" +import { ConfigMedia } from "@opencode-ai/schema/config/media" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FileSystem } from "@opencode-ai/core/filesystem" @@ -429,9 +430,9 @@ describe("ReadTool", () => { } const configTest = yield* Config.Test yield* configTest.setEntries([ - new Config.Document({ + new Document({ type: "document", - info: new Config.Info({ + info: new Info({ media: new ConfigMedia.Info({ image: new ConfigMedia.Image({ auto_resize: false, max_width: 4 }), }), @@ -472,9 +473,9 @@ describe("ReadTool", () => { } const configTest = yield* Config.Test yield* configTest.setEntries([ - new Config.Document({ + new Document({ type: "document", - info: new Config.Info({ + info: new Info({ media: new ConfigMedia.Info({ image: new ConfigMedia.Image({ max_width: 4 }) }), }), }), @@ -511,9 +512,9 @@ describe("ReadTool", () => { } const configTest = yield* Config.Test yield* configTest.setEntries([ - new Config.Document({ + new Document({ type: "document", - info: new Config.Info({ + info: new Info({ media: new ConfigMedia.Info({ image: new ConfigMedia.Image({ max_base64_bytes: 1 }), }), diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index b33dfeb13be..51b542a6ff0 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -1025,14 +1025,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -1540,14 +1533,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -2912,14 +2898,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -3009,14 +2988,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -3586,14 +3558,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -3820,14 +3785,11 @@ "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, { "$ref": "#/components/schemas/MessageNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -3956,14 +3918,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -11771,39 +11726,101 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "required", - "running", - "completed" - ] - }, - "completed": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "required", + "completed" + ] } - ] + }, + "required": [ + "status" + ], + "additionalProperties": false }, - "total": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running" + ] + }, + "progress": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "numerator": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "denominator": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "label" + ], + "additionalProperties": false } - ] + }, + "required": [ + "status", + "progress" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false } - }, - "required": [ - "status", - "completed", - "total" - ], - "additionalProperties": false + ] } } } @@ -11831,60 +11848,6 @@ }, "description": "Return the progress of the V1 to V2 session history migration.", "summary": "Get V1 migration status" - }, - "post": { - "tags": [ - "migration" - ], - "operationId": "v2.experimental.migration.v1.run", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "completed" - ] - } - }, - "required": [ - "status" - ], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Run or resume the V1 to V2 session history migration and wait for completion.", - "summary": "Run V1 migration" } }, "/api/websearch/provider": { @@ -12134,6 +12097,94 @@ "required": true } } + }, + "/api/config": { + "get": { + "tags": [ + "config" + ], + "operationId": "v2.config.get", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Config.Entry" + } + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Return configuration documents and discovery sources for the requested location, from lowest to highest priority.", + "summary": "Get configuration" + } } }, "components": { @@ -16014,6 +16065,9 @@ } ] } + }, + "text": { + "type": "string" } }, "required": [ @@ -26457,6 +26511,1744 @@ "results" ], "additionalProperties": false + }, + "Config.ModelSelection": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[^/#]+\\/[^#]+(?:#[^#]+)?$" + } + ] + }, + { + "type": "object", + "properties": { + "providerID": { + "type": "string", + "allOf": [ + { + "pattern": "^[^/#]+$" + } + ] + }, + "model": { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + }, + "variant": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "providerID", + "model" + ], + "additionalProperties": false + } + ] + }, + "Config.Provider.Request": { + "type": "object", + "properties": { + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Agent": { + "type": "object", + "properties": { + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ModelSelection" + }, + { + "type": "null" + } + ] + }, + "request": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Provider.Request" + }, + { + "type": "null" + } + ] + }, + "system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + { + "type": "null" + } + ] + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^#[0-9a-fA-F]{6}$" + } + ] + }, + { + "type": "null" + } + ] + }, + "steps": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "permissions": { + "anyOf": [ + { + "$ref": "#/components/schemas/Permission.Ruleset" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Watcher": { + "type": "object", + "properties": { + "ignore": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Formatter.Entry": { + "type": "object", + "properties": { + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "command": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "environment": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "extensions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.LSP.Server": { + "type": "object", + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "extensions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "env": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "initialization": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "command" + ], + "additionalProperties": false + }, + "Config.Media.Image": { + "type": "object", + "properties": { + "auto_resize": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "max_width": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "max_height": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "max_base64_bytes": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Media": { + "type": "object", + "properties": { + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Media.Image" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.ToolOutput": { + "type": "object", + "properties": { + "max_lines": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "max_bytes": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.MCP": { + "type": "object", + "properties": { + "timeout": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.TimeoutConfig" + }, + { + "type": "null" + } + ] + }, + "servers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.LocalConfig" + }, + { + "$ref": "#/components/schemas/Mcp.RemoteConfig" + } + ] + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Compaction.Keep": { + "type": "object", + "properties": { + "tokens": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Compaction": { + "type": "object", + "properties": { + "auto": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "keep": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Compaction.Keep" + }, + { + "type": "null" + } + ] + }, + "buffer": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Command": { + "type": "object", + "properties": { + "template": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ModelSelection" + }, + { + "type": "null" + } + ] + }, + "subtask": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "template" + ], + "additionalProperties": false + }, + "Config.Reference.Git": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "repository" + ], + "additionalProperties": false + }, + "Config.Reference.Local": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + "ConfigWebSearch.Info": { + "type": "object", + "properties": { + "provider": { + "type": "string" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + "Config.Plugin.Entry": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "options": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "package" + ], + "additionalProperties": false + }, + "Config.Warming": { + "type": "object", + "properties": { + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Prompt sent for keep-alive requests" + }, + "interval": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Idle time between keep-alive requests (default: \"4 minutes\")" + }, + "duration": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Time after the last active request to keep a session warm (default: \"30 minutes\")" + } + }, + "additionalProperties": false + }, + "Config.Model.Cost.Cache": { + "type": "object", + "properties": { + "read": { + "anyOf": [ + { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + { + "type": "null" + } + ] + }, + "write": { + "anyOf": [ + { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Model.Cost": { + "type": "object", + "properties": { + "tier": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "context" + ] + }, + "size": { + "type": "integer" + } + }, + "required": [ + "type", + "size" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "input": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "output": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "cache": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Model.Cost.Cache" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "input", + "output" + ], + "additionalProperties": false + }, + "Config.Model.Limit": { + "type": "object", + "properties": { + "context": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "input": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "output": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Model": { + "type": "object", + "properties": { + "modelID": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "family": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "compatibility": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Compatibility" + }, + { + "type": "null" + } + ] + }, + "package": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "settings": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "capabilities": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Capabilities" + }, + { + "type": "null" + } + ] + }, + "variants": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + { + "type": "null" + } + ] + }, + "cost": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Model.Cost" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Config.Model.Cost" + } + } + ] + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "limit": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Model.Limit" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Provider": { + "type": "object", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "env": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "package": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "settings": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "models": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Model" + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ConfigExperimental.Info": { + "type": "object", + "properties": { + "subagent_depth": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum subagent nesting depth. Defaults to 1." + }, + "policies": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "provider.use" + ] + }, + "resource": { + "type": "string" + }, + "effect": { + "type": "string", + "enum": [ + "allow", + "deny" + ] + } + }, + "required": [ + "action", + "resource", + "effect" + ], + "additionalProperties": false + } + }, + { + "type": "null" + } + ], + "description": "Ordered policies controlling access to configured resources" + } + }, + "additionalProperties": false + }, + "Config.Info": { + "type": "object", + "properties": { + "$schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "JSON schema reference for configuration validation" + }, + "shell": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Default shell to use for terminal and shell tool execution" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ModelSelection" + }, + { + "type": "null" + } + ], + "description": "Default model to use when no session or agent model is selected" + }, + "default_agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Default primary agent to use when no session agent is selected" + }, + "autoupdate": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": [ + "notify" + ] + } + ] + }, + { + "type": "null" + } + ], + "description": "Automatically update or notify when a new version is available" + }, + "share": { + "anyOf": [ + { + "type": "string", + "enum": [ + "manual", + "auto", + "disabled" + ] + }, + { + "type": "null" + } + ], + "description": "Control whether sessions may be shared manually, automatically, or not at all" + }, + "enterprise": { + "anyOf": [ + { + "type": "object", + "properties": { + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ], + "description": "Enterprise sharing service configuration" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Username displayed in conversations and used for telemetry identity" + }, + "permissions": { + "anyOf": [ + { + "$ref": "#/components/schemas/Permission.Ruleset" + }, + { + "type": "null" + } + ], + "description": "Ordered tool permission rules applied to agent tool use" + }, + "agents": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Agent" + } + }, + { + "type": "null" + } + ], + "description": "Named built-in agent overrides and custom agent definitions" + }, + "snapshots": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Enable snapshots used for undo and revert behavior" + }, + "watcher": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Watcher" + }, + { + "type": "null" + } + ], + "description": "Filesystem watcher configuration" + }, + "formatter": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Formatter.Entry" + } + } + ] + }, + { + "type": "null" + } + ], + "description": "Enable built-in formatters or configure formatter overrides" + }, + "lsp": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "disabled" + ], + "additionalProperties": false + }, + { + "$ref": "#/components/schemas/Config.LSP.Server" + } + ] + } + } + ] + }, + { + "type": "null" + } + ], + "description": "Enable built-in language servers or configure server overrides" + }, + "media": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Media" + }, + { + "type": "null" + } + ], + "description": "Media processing configuration" + }, + "tool_output": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ToolOutput" + }, + { + "type": "null" + } + ], + "description": "Tool output truncation thresholds" + }, + "mcp": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.MCP" + }, + { + "type": "null" + } + ], + "description": "MCP server configuration" + }, + "compaction": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Compaction" + }, + { + "type": "null" + } + ], + "description": "Conversation compaction behavior" + }, + "skills": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Additional paths or URLs to discover skills from" + }, + "commands": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Command" + } + }, + { + "type": "null" + } + ], + "description": "Named slash command definitions" + }, + "instructions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Additional paths or URLs supplying ambient instructions" + }, + "references": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/Config.Reference.Git" + }, + { + "$ref": "#/components/schemas/Config.Reference.Local" + } + ] + } + }, + { + "type": "null" + } + ], + "description": "Named local directories or Git repositories available as external context" + }, + "websearch": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConfigWebSearch.Info" + }, + { + "type": "null" + } + ], + "description": "Web search provider selection" + }, + "plugins": { + "anyOf": [ + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/Config.Plugin.Entry" + } + ] + } + }, + { + "type": "null" + } + ], + "description": "Ordered plugin enablement directives and external package declarations" + }, + "warming": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/Config.Warming" + } + ] + }, + { + "type": "null" + } + ], + "description": "Keep recently active sessions warm with transient model requests (default: false)" + }, + "providers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Provider" + } + }, + { + "type": "null" + } + ] + }, + "experimental": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConfigExperimental.Info" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Document": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "document" + ] + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Config.Info" + } + }, + "required": [ + "type", + "info" + ], + "additionalProperties": false + }, + "Config.Directory": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "directory" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.File": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.AgentsDirectory": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agents" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.ClaudeDirectory": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "claude" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.Entry": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Document" + }, + { + "$ref": "#/components/schemas/Config.Directory" + }, + { + "$ref": "#/components/schemas/Config.File" + }, + { + "$ref": "#/components/schemas/Config.AgentsDirectory" + }, + { + "$ref": "#/components/schemas/Config.ClaudeDirectory" + } + ] } }, "securitySchemes": {} @@ -26571,6 +28363,10 @@ { "name": "websearch", "description": "Location-scoped web search routes." + }, + { + "name": "config", + "description": "Location-scoped configuration routes." } ] } diff --git a/packages/protocol/src/api.ts b/packages/protocol/src/api.ts index 6534f132439..7e9b279527e 100644 --- a/packages/protocol/src/api.ts +++ b/packages/protocol/src/api.ts @@ -32,6 +32,7 @@ import { ProjectGroup } from "./groups/project.js" import { ProjectCopyGroup } from "./groups/project-copy.js" import { VcsGroup } from "./groups/vcs.js" import { MigrationGroup } from "./groups/migration.js" +import { ConfigGroup } from "./groups/config.js" type LocationGroups = | HttpApiGroup.AddMiddleware @@ -53,6 +54,7 @@ type LocationGroups = | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware + | HttpApiGroup.AddMiddleware type SessionGroups = | ReturnType> @@ -175,6 +177,7 @@ const makeApiFromGroup = < .add(DebugGroup) .add(MigrationGroup) .add(WebSearchGroup.middleware(locationMiddleware)) + .add(ConfigGroup.middleware(locationMiddleware)) .annotateMerge( OpenApi.annotations({ title: "opencode HttpApi", diff --git a/packages/protocol/src/client.ts b/packages/protocol/src/client.ts index 9a809606353..d84aeeeb4aa 100644 --- a/packages/protocol/src/client.ts +++ b/packages/protocol/src/client.ts @@ -62,6 +62,7 @@ export const groupNames = { "server.project": "project", "server.projectCopy": "projectCopy", "server.vcs": "vcs", + "server.config": "config", } as const export const promiseOmitEndpoints = new Set(["pty.connect", "pty.connectToken"]) diff --git a/packages/protocol/src/groups/config.ts b/packages/protocol/src/groups/config.ts new file mode 100644 index 00000000000..b078ad9042e --- /dev/null +++ b/packages/protocol/src/groups/config.ts @@ -0,0 +1,22 @@ +import { Config } from "@opencode-ai/schema/config" +import { Schema } from "effect" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { LocationQuery, locationQueryOpenApi } from "./location.js" + +export const ConfigGroup = HttpApiGroup.make("server.config") + .add( + HttpApiEndpoint.get("config.get", "/api/config", { + query: LocationQuery, + success: Schema.Array(Config.Entry), + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.config.get", + summary: "Get configuration", + description: + "Return configuration documents and discovery sources for the requested location, from lowest to highest priority.", + }), + ), + ) + .annotateMerge(OpenApi.annotations({ title: "config", description: "Location-scoped configuration routes." })) diff --git a/packages/schema/src/config.ts b/packages/schema/src/config.ts index 92b07b0e39e..17c2fbc9f6e 100644 --- a/packages/schema/src/config.ts +++ b/packages/schema/src/config.ts @@ -1,6 +1,142 @@ export * as Config from "./config.js" +import { Schema } from "effect" import { ephemeral, inventory } from "./event.js" +import { Permission } from "./permission.js" +import { AbsolutePath } from "./schema.js" +import { ConfigAgent } from "./config/agent.js" +import { ConfigMedia } from "./config/media.js" +import { ConfigCompaction } from "./config/compaction.js" +import { ConfigCommand } from "./config/command.js" +import { ConfigExperimental } from "./config/experimental.js" +import { ConfigFormatter } from "./config/formatter.js" +import { ConfigLSP } from "./config/lsp.js" +import { ConfigMCP } from "./config/mcp.js" +import { ConfigModel } from "./config/model.js" +import { ConfigPlugin } from "./config/plugin.js" +import { ConfigProvider } from "./config/provider.js" +import { ConfigReference } from "./config/reference.js" +import { ConfigWebSearch } from "./config/websearch.js" +import { ConfigToolOutput } from "./config/tool-output.js" +import { ConfigWatcher } from "./config/watcher.js" +import { ConfigWarming } from "./config/warming.js" + +export class Info extends Schema.Class("Config.Info")({ + $schema: Schema.optional(Schema.String).annotate({ + description: "JSON schema reference for configuration validation", + }), + shell: Schema.String.pipe(Schema.optional).annotate({ + description: "Default shell to use for terminal and shell tool execution", + }), + model: ConfigModel.Selection.pipe(Schema.optional).annotate({ + description: "Default model to use when no session or agent model is selected", + }), + default_agent: Schema.String.pipe(Schema.optional).annotate({ + description: "Default primary agent to use when no session agent is selected", + }), + autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")]) + .pipe(Schema.optional) + .annotate({ + description: "Automatically update or notify when a new version is available", + }), + share: Schema.Literals(["manual", "auto", "disabled"]).pipe(Schema.optional).annotate({ + description: "Control whether sessions may be shared manually, automatically, or not at all", + }), + enterprise: Schema.Struct({ + url: Schema.String.pipe(Schema.optional), + }) + .pipe(Schema.optional) + .annotate({ + description: "Enterprise sharing service configuration", + }), + username: Schema.String.pipe(Schema.optional).annotate({ + description: "Username displayed in conversations and used for telemetry identity", + }), + permissions: Permission.Ruleset.pipe(Schema.optional).annotate({ + description: "Ordered tool permission rules applied to agent tool use", + }), + agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({ + description: "Named built-in agent overrides and custom agent definitions", + }), + snapshots: Schema.Boolean.pipe(Schema.optional).annotate({ + description: "Enable snapshots used for undo and revert behavior", + }), + watcher: ConfigWatcher.Info.pipe(Schema.optional).annotate({ + description: "Filesystem watcher configuration", + }), + formatter: ConfigFormatter.Info.pipe(Schema.optional).annotate({ + description: "Enable built-in formatters or configure formatter overrides", + }), + lsp: ConfigLSP.Info.pipe(Schema.optional).annotate({ + description: "Enable built-in language servers or configure server overrides", + }), + media: ConfigMedia.Info.pipe(Schema.optional).annotate({ + description: "Media processing configuration", + }), + tool_output: ConfigToolOutput.Info.pipe(Schema.optional).annotate({ + description: "Tool output truncation thresholds", + }), + mcp: ConfigMCP.Info.pipe(Schema.optional).annotate({ + description: "MCP server configuration", + }), + compaction: ConfigCompaction.Info.pipe(Schema.optional).annotate({ + description: "Conversation compaction behavior", + }), + skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ + description: "Additional paths or URLs to discover skills from", + }), + commands: Schema.Record(Schema.String, ConfigCommand.Info).pipe(Schema.optional).annotate({ + description: "Named slash command definitions", + }), + instructions: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ + description: "Additional paths or URLs supplying ambient instructions", + }), + references: ConfigReference.Info.pipe(Schema.optional).annotate({ + description: "Named local directories or Git repositories available as external context", + }), + websearch: ConfigWebSearch.Info.pipe(Schema.optional).annotate({ + description: "Web search provider selection", + }), + plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({ + description: "Ordered plugin enablement directives and external package declarations", + }), + warming: ConfigWarming.Warming.pipe(Schema.optional).annotate({ + description: "Keep recently active sessions warm with transient model requests (default: false)", + }), + providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional), + experimental: ConfigExperimental.Info.pipe(Schema.optional), +}) {} + +export class Document extends Schema.Class("Config.Document")({ + type: Schema.Literal("document"), + path: Schema.String.pipe(Schema.optional), + info: Info, +}) {} + +export class Directory extends Schema.Class("Config.Directory")({ + type: Schema.Literal("directory"), + path: AbsolutePath, +}) {} + +export class File extends Schema.Class("Config.File")({ + type: Schema.Literal("file"), + path: AbsolutePath, +}) {} + +export class AgentsDirectory extends Schema.Class("Config.AgentsDirectory")({ + type: Schema.Literal("agents"), + path: AbsolutePath, +}) {} + +export class ClaudeDirectory extends Schema.Class("Config.ClaudeDirectory")({ + type: Schema.Literal("claude"), + path: AbsolutePath, +}) {} + +export const Entry = Schema.Union([Document, Directory, File, AgentsDirectory, ClaudeDirectory]).annotate({ + identifier: "Config.Entry", +}) +export type Entry = typeof Entry.Type const Updated = ephemeral({ type: "config.updated", diff --git a/packages/core/src/config/agent.ts b/packages/schema/src/config/agent.ts similarity index 76% rename from packages/core/src/config/agent.ts rename to packages/schema/src/config/agent.ts index 94c7efb97b3..704ac92625d 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/schema/src/config/agent.ts @@ -1,10 +1,10 @@ -export * as ConfigAgent from "./agent" +export * as ConfigAgent from "./agent.js" import { Schema } from "effect" -import { Permission } from "@opencode-ai/schema/permission" -import { ConfigProvider } from "./provider" -import { ConfigModel } from "./model" -import { PositiveInt } from "../schema" +import { Permission } from "../permission.js" +import { PositiveInt } from "../schema.js" +import { ConfigModel } from "./model.js" +import { ConfigProvider } from "./provider.js" export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) diff --git a/packages/core/src/config/command.ts b/packages/schema/src/config/command.ts similarity index 79% rename from packages/core/src/config/command.ts rename to packages/schema/src/config/command.ts index ff9fdf926ad..05caa2fbe6c 100644 --- a/packages/core/src/config/command.ts +++ b/packages/schema/src/config/command.ts @@ -1,7 +1,7 @@ -export * as ConfigCommand from "./command" +export * as ConfigCommand from "./command.js" import { Schema } from "effect" -import { ConfigModel } from "./model" +import { ConfigModel } from "./model.js" export class Info extends Schema.Class("Config.Command")({ template: Schema.String, diff --git a/packages/core/src/config/compaction.ts b/packages/schema/src/config/compaction.ts similarity index 78% rename from packages/core/src/config/compaction.ts rename to packages/schema/src/config/compaction.ts index c52e0da7998..6ad7b9d3a2a 100644 --- a/packages/core/src/config/compaction.ts +++ b/packages/schema/src/config/compaction.ts @@ -1,7 +1,7 @@ -export * as ConfigCompaction from "./compaction" +export * as ConfigCompaction from "./compaction.js" import { Schema } from "effect" -import { NonNegativeInt } from "../schema" +import { NonNegativeInt } from "../schema.js" export class Keep extends Schema.Class("Config.Compaction.Keep")({ tokens: NonNegativeInt.pipe(Schema.optional), diff --git a/packages/core/src/config/experimental.ts b/packages/schema/src/config/experimental.ts similarity index 74% rename from packages/core/src/config/experimental.ts rename to packages/schema/src/config/experimental.ts index d8ecbbb7623..ee583506ee1 100644 --- a/packages/core/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -1,8 +1,8 @@ -export * as ConfigExperimental from "./experimental" +export * as ConfigExperimental from "./experimental.js" import { Schema } from "effect" -import { NonNegativeInt } from "../schema" -import { ConfigPolicy } from "./policy" +import { NonNegativeInt } from "../schema.js" +import { ConfigPolicy } from "./policy.js" export class Info extends Schema.Class("ConfigExperimental.Info")({ subagent_depth: NonNegativeInt.pipe(Schema.optional).annotate({ diff --git a/packages/core/src/config/formatter.ts b/packages/schema/src/config/formatter.ts similarity index 90% rename from packages/core/src/config/formatter.ts rename to packages/schema/src/config/formatter.ts index 5730ceeec68..facd57b8e8d 100644 --- a/packages/core/src/config/formatter.ts +++ b/packages/schema/src/config/formatter.ts @@ -1,4 +1,4 @@ -export * as ConfigFormatter from "./formatter" +export * as ConfigFormatter from "./formatter.js" import { Schema } from "effect" diff --git a/packages/core/src/config/lsp.ts b/packages/schema/src/config/lsp.ts similarity index 94% rename from packages/core/src/config/lsp.ts rename to packages/schema/src/config/lsp.ts index 14c37718c9a..09567b5f20d 100644 --- a/packages/core/src/config/lsp.ts +++ b/packages/schema/src/config/lsp.ts @@ -1,4 +1,4 @@ -export * as ConfigLSP from "./lsp" +export * as ConfigLSP from "./lsp.js" import { Schema } from "effect" diff --git a/packages/core/src/config/mcp.ts b/packages/schema/src/config/mcp.ts similarity index 71% rename from packages/core/src/config/mcp.ts rename to packages/schema/src/config/mcp.ts index 96bb2978b8b..0b5ae4ace02 100644 --- a/packages/core/src/config/mcp.ts +++ b/packages/schema/src/config/mcp.ts @@ -1,10 +1,8 @@ -export * as ConfigMCP from "./mcp" +export * as ConfigMCP from "./mcp.js" import { Schema } from "effect" -import { Mcp } from "@opencode-ai/schema/mcp" +import { Mcp } from "../mcp.js" -// The MCP server config is a public wire contract (used by the mcp.add route), so it lives in -// @opencode-ai/schema and is re-exported here. export const Timeout = Mcp.TimeoutConfig export type Timeout = Mcp.TimeoutConfig export const Local = Mcp.LocalConfig diff --git a/packages/core/src/config/media.ts b/packages/schema/src/config/media.ts similarity index 83% rename from packages/core/src/config/media.ts rename to packages/schema/src/config/media.ts index fa5dad76308..f2f858d6596 100644 --- a/packages/core/src/config/media.ts +++ b/packages/schema/src/config/media.ts @@ -1,7 +1,7 @@ -export * as ConfigMedia from "./media" +export * as ConfigMedia from "./media.js" import { Schema } from "effect" -import { PositiveInt } from "../schema" +import { PositiveInt } from "../schema.js" export class Image extends Schema.Class("Config.Media.Image")({ auto_resize: Schema.Boolean.pipe(Schema.optional), diff --git a/packages/core/src/config/model.ts b/packages/schema/src/config/model.ts similarity index 87% rename from packages/core/src/config/model.ts rename to packages/schema/src/config/model.ts index e3e1d94e840..e7154d7c68f 100644 --- a/packages/core/src/config/model.ts +++ b/packages/schema/src/config/model.ts @@ -1,8 +1,8 @@ -export * as ConfigModel from "./model" +export * as ConfigModel from "./model.js" import { Schema, SchemaGetter } from "effect" -import { Model } from "@opencode-ai/schema/model" -import { Provider } from "@opencode-ai/schema/provider" +import { Model } from "../model.js" +import { Provider } from "../provider.js" const ProviderID = Provider.ID.check(Schema.isPattern(/^[^/#]+$/)) const ModelID = Model.ID.check(Schema.isPattern(/^[^#]+$/)) diff --git a/packages/core/src/config/plugin.ts b/packages/schema/src/config/plugin.ts similarity index 89% rename from packages/core/src/config/plugin.ts rename to packages/schema/src/config/plugin.ts index 4268a7f79d2..bc28165430b 100644 --- a/packages/core/src/config/plugin.ts +++ b/packages/schema/src/config/plugin.ts @@ -1,4 +1,4 @@ -export * as ConfigPlugin from "./plugin" +export * as ConfigPlugin from "./plugin.js" import { Schema } from "effect" diff --git a/packages/core/src/config/policy.ts b/packages/schema/src/config/policy.ts similarity index 86% rename from packages/core/src/config/policy.ts rename to packages/schema/src/config/policy.ts index 127bc822a30..2c3e2707072 100644 --- a/packages/core/src/config/policy.ts +++ b/packages/schema/src/config/policy.ts @@ -1,4 +1,4 @@ -export * as ConfigPolicy from "./policy" +export * as ConfigPolicy from "./policy.js" import { Schema } from "effect" diff --git a/packages/core/src/config/provider.ts b/packages/schema/src/config/provider.ts similarity index 95% rename from packages/core/src/config/provider.ts rename to packages/schema/src/config/provider.ts index feb92a5c5e5..b3eba949caa 100644 --- a/packages/core/src/config/provider.ts +++ b/packages/schema/src/config/provider.ts @@ -1,8 +1,8 @@ -export * as ConfigProvider from "./provider" +export * as ConfigProvider from "./provider.js" import { Schema } from "effect" -import { Money } from "@opencode-ai/schema/money" -import { Capabilities, Compatibility, Family, ID, VariantID } from "../model" +import { Money } from "../money.js" +import { Capabilities, Compatibility, Family, ID, VariantID } from "../model.js" const JsonRecord = Schema.Record(Schema.String, Schema.Json) diff --git a/packages/core/src/config/reference.ts b/packages/schema/src/config/reference.ts similarity index 93% rename from packages/core/src/config/reference.ts rename to packages/schema/src/config/reference.ts index d2fabb0a899..7e1e8cf0dec 100644 --- a/packages/core/src/config/reference.ts +++ b/packages/schema/src/config/reference.ts @@ -1,4 +1,4 @@ -export * as ConfigReference from "./reference" +export * as ConfigReference from "./reference.js" import { Schema } from "effect" diff --git a/packages/core/src/config/tool-output.ts b/packages/schema/src/config/tool-output.ts similarity index 68% rename from packages/core/src/config/tool-output.ts rename to packages/schema/src/config/tool-output.ts index 0eac04307dc..68ba0526983 100644 --- a/packages/core/src/config/tool-output.ts +++ b/packages/schema/src/config/tool-output.ts @@ -1,7 +1,7 @@ -export * as ConfigToolOutput from "./tool-output" +export * as ConfigToolOutput from "./tool-output.js" import { Schema } from "effect" -import { PositiveInt } from "../schema" +import { PositiveInt } from "../schema.js" export class Info extends Schema.Class("Config.ToolOutput")({ max_lines: PositiveInt.pipe(Schema.optional), diff --git a/packages/core/src/config/warming.ts b/packages/schema/src/config/warming.ts similarity index 93% rename from packages/core/src/config/warming.ts rename to packages/schema/src/config/warming.ts index cb90afc2eaf..8f0ab1a546e 100644 --- a/packages/core/src/config/warming.ts +++ b/packages/schema/src/config/warming.ts @@ -1,4 +1,4 @@ -export * as ConfigWarming from "./warming" +export * as ConfigWarming from "./warming.js" import { Schema } from "effect" diff --git a/packages/core/src/config/watcher.ts b/packages/schema/src/config/watcher.ts similarity index 78% rename from packages/core/src/config/watcher.ts rename to packages/schema/src/config/watcher.ts index be5c91a9bfe..78d6ddbe60a 100644 --- a/packages/core/src/config/watcher.ts +++ b/packages/schema/src/config/watcher.ts @@ -1,4 +1,4 @@ -export * as ConfigWatcher from "./watcher" +export * as ConfigWatcher from "./watcher.js" import { Schema } from "effect" diff --git a/packages/core/src/config/websearch.ts b/packages/schema/src/config/websearch.ts similarity index 56% rename from packages/core/src/config/websearch.ts rename to packages/schema/src/config/websearch.ts index 4db2a50fc64..c2a3cc7bdd3 100644 --- a/packages/core/src/config/websearch.ts +++ b/packages/schema/src/config/websearch.ts @@ -1,7 +1,7 @@ -export * as ConfigWebSearch from "./websearch" +export * as ConfigWebSearch from "./websearch.js" -import { WebSearch } from "@opencode-ai/schema/websearch" import { Schema } from "effect" +import { WebSearch } from "../websearch.js" export class Info extends Schema.Class("ConfigWebSearch.Info")({ provider: WebSearch.ID, diff --git a/packages/schema/test/config.test.ts b/packages/schema/test/config.test.ts new file mode 100644 index 00000000000..e58723993d1 --- /dev/null +++ b/packages/schema/test/config.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from "bun:test" +import { Schema } from "effect" +import { Config } from "../src/config.js" +import { AbsolutePath } from "../src/schema.js" + +describe("Config.Entry", () => { + test("round-trips every configuration entry type", () => { + const entries = [ + new Config.Document({ + type: "document", + path: "/project/opencode.json", + info: new Config.Info({ + permissions: [ + { action: "shell", resource: "*", effect: "ask" }, + { action: "shell", resource: "git status", effect: "allow" }, + ], + }), + }), + new Config.Document({ type: "document", info: new Config.Info({ shell: "/bin/zsh" }) }), + new Config.Directory({ type: "directory", path: AbsolutePath.make("/project/.opencode") }), + new Config.File({ type: "file", path: AbsolutePath.make("/project/opencode.json") }), + new Config.AgentsDirectory({ type: "agents", path: AbsolutePath.make("/project/.agents") }), + new Config.ClaudeDirectory({ type: "claude", path: AbsolutePath.make("/project/.claude") }), + ] + + const encoded = Schema.encodeSync(Schema.Array(Config.Entry))(entries) + const decoded = Schema.decodeUnknownSync(Schema.Array(Config.Entry))(encoded) + + expect(decoded).toEqual(entries) + expect(decoded[0]).toBeInstanceOf(Config.Document) + expect(decoded[1]).not.toHaveProperty("path") + expect(decoded.map((entry) => entry.type)).toEqual(["document", "document", "directory", "file", "agents", "claude"]) + expect(decoded[0]?.type === "document" ? decoded[0].info.permissions : undefined).toEqual([ + { action: "shell", resource: "*", effect: "ask" }, + { action: "shell", resource: "git status", effect: "allow" }, + ]) + }) + + test("has a stable public identifier", () => { + expect(Config.Entry.ast.annotations?.identifier).toBe("Config.Entry") + }) +}) diff --git a/packages/sdk-next/src/index.ts b/packages/sdk-next/src/index.ts index 2693877bba5..c6acc4cc153 100644 --- a/packages/sdk-next/src/index.ts +++ b/packages/sdk-next/src/index.ts @@ -5,6 +5,7 @@ export { ClientError } from "@opencode-ai/client/effect" export type { OpenCodeEvent } from "@opencode-ai/client/effect" export { Agent } from "@opencode-ai/schema/agent" export { Command } from "@opencode-ai/schema/command" +export { Config } from "@opencode-ai/schema/config" export { Credential } from "@opencode-ai/schema/credential" export { FileSystem } from "@opencode-ai/schema/filesystem" export { Integration } from "@opencode-ai/schema/integration" diff --git a/packages/sdk-next/test/contract-identity.test.ts b/packages/sdk-next/test/contract-identity.test.ts index afba8f58add..60b0e99e150 100644 --- a/packages/sdk-next/test/contract-identity.test.ts +++ b/packages/sdk-next/test/contract-identity.test.ts @@ -3,6 +3,7 @@ import { Location as CoreLocation } from "@opencode-ai/core/location" import { SessionPending as CoreSessionPending } from "@opencode-ai/core/session/pending" import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message" import { Agent } from "@opencode-ai/schema/agent" +import { Config } from "@opencode-ai/schema/config" import { Location } from "@opencode-ai/schema/location" import { Model } from "@opencode-ai/schema/model" import { Project } from "@opencode-ai/schema/project" @@ -24,6 +25,7 @@ const CoreSession = await import("@opencode-ai/core/session") test("re-exports canonical contracts directly from Schema", () => { expect(SDK.Agent).toBe(Agent) + expect(SDK.Config).toBe(Config) expect(SDK.Model).toBe(Model) expect(SDK.WebSearch).toBe(WebSearch) expect(SDK.Session).toBe(Session) @@ -32,6 +34,7 @@ test("re-exports canonical contracts directly from Schema", () => { "Agent", "ClientError", "Command", + "Config", "Credential", "FileSystem", "Integration", diff --git a/packages/server/src/handlers.ts b/packages/server/src/handlers.ts index 7f96b05fce9..28f29166fd8 100644 --- a/packages/server/src/handlers.ts +++ b/packages/server/src/handlers.ts @@ -29,6 +29,7 @@ import { ProjectCopyHandler } from "./handlers/project-copy" import { VcsHandler } from "./handlers/vcs" import { EventFeed } from "./event-feed" import { MigrationHandler } from "./handlers/migration" +import { ConfigHandler } from "./handlers/config" export const handlers = Layer.mergeAll( HealthHandler, @@ -60,4 +61,5 @@ export const handlers = Layer.mergeAll( ReferenceHandler, ProjectCopyHandler, VcsHandler, + ConfigHandler, ) diff --git a/packages/server/src/handlers/config.ts b/packages/server/src/handlers/config.ts new file mode 100644 index 00000000000..1509c4ecd5f --- /dev/null +++ b/packages/server/src/handlers/config.ts @@ -0,0 +1,7 @@ +import { Config } from "@opencode-ai/core/config" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { Api } from "../api" + +export const ConfigHandler = HttpApiBuilder.group(Api, "server.config", (handlers) => + handlers.handle("config.get", () => Config.Service.use((config) => config.entries())), +) diff --git a/packages/server/test/config.test.ts b/packages/server/test/config.test.ts new file mode 100644 index 00000000000..4ab941986c5 --- /dev/null +++ b/packages/server/test/config.test.ts @@ -0,0 +1,62 @@ +import fs from "node:fs/promises" +import path from "node:path" +import { expect } from "bun:test" +import { Config } from "@opencode-ai/schema/config" +import { Effect, Schema } from "effect" +import { HttpServer } from "effect/unstable/http" +import { tmpdir } from "../../core/test/fixture/tmpdir" +import { it } from "../../core/test/lib/effect" +import { ServerProcess } from "../src/process" + +it.live("returns ordered config entries for the requested directory", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir("opencode-config-endpoint-")), + (tmp) => + Effect.gen(function* () { + const global = path.join(tmp.path, "global") + const project = path.join(tmp.path, "project") + const config = path.join(project, "opencode.json") + yield* Effect.promise(() => Promise.all([fs.mkdir(global, { recursive: true }), fs.mkdir(project, { recursive: true })])) + yield* Effect.promise(() => + fs.writeFile( + config, + JSON.stringify({ + permissions: [ + { action: "shell", resource: "*", effect: "ask" }, + { action: "shell", resource: "git status", effect: "allow" }, + ], + }), + ), + ) + const server = yield* ServerProcess.start({ + hostname: "127.0.0.1", + port: 0, + password: "secret", + app: { version: "test-version" }, + database: { path: ":memory:" }, + config: { directory: global }, + fs: { filewatcher: false }, + }) + const url = new URL("/api/config", HttpServer.formatAddress(server.address)) + url.searchParams.set("location[directory]", project) + const response = yield* Effect.promise(() => + fetch(url, { headers: { authorization: `Basic ${btoa("opencode:secret")}` } }), + ) + const entries = Schema.decodeUnknownSync(Schema.Array(Config.Entry))( + yield* Effect.promise(() => response.json()), + ) + + expect(response.status).toBe(200) + expect(Array.isArray(entries)).toBe(true) + const document = entries.find( + (entry): entry is Config.Document => entry.type === "document" && entry.path === config, + ) + expect(document?.info.permissions).toEqual([ + { action: "shell", resource: "*", effect: "ask" }, + { action: "shell", resource: "git status", effect: "allow" }, + ]) + expect(entries.some((entry) => entry.type === "file" && entry.path === config)).toBe(true) + }), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), +) diff --git a/packages/www/openapi.json b/packages/www/openapi.json index b33dfeb13be..51b542a6ff0 100644 --- a/packages/www/openapi.json +++ b/packages/www/openapi.json @@ -1025,14 +1025,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -1540,14 +1533,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -2912,14 +2898,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -3009,14 +2988,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -3586,14 +3558,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -3820,14 +3785,11 @@ "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, { "$ref": "#/components/schemas/MessageNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -3956,14 +3918,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -11771,39 +11726,101 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "required", - "running", - "completed" - ] - }, - "completed": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "required", + "completed" + ] } - ] + }, + "required": [ + "status" + ], + "additionalProperties": false }, - "total": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running" + ] + }, + "progress": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "numerator": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "denominator": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "label" + ], + "additionalProperties": false } - ] + }, + "required": [ + "status", + "progress" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false } - }, - "required": [ - "status", - "completed", - "total" - ], - "additionalProperties": false + ] } } } @@ -11831,60 +11848,6 @@ }, "description": "Return the progress of the V1 to V2 session history migration.", "summary": "Get V1 migration status" - }, - "post": { - "tags": [ - "migration" - ], - "operationId": "v2.experimental.migration.v1.run", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "completed" - ] - } - }, - "required": [ - "status" - ], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Run or resume the V1 to V2 session history migration and wait for completion.", - "summary": "Run V1 migration" } }, "/api/websearch/provider": { @@ -12134,6 +12097,94 @@ "required": true } } + }, + "/api/config": { + "get": { + "tags": [ + "config" + ], + "operationId": "v2.config.get", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Config.Entry" + } + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Return configuration documents and discovery sources for the requested location, from lowest to highest priority.", + "summary": "Get configuration" + } } }, "components": { @@ -16014,6 +16065,9 @@ } ] } + }, + "text": { + "type": "string" } }, "required": [ @@ -26457,6 +26511,1744 @@ "results" ], "additionalProperties": false + }, + "Config.ModelSelection": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[^/#]+\\/[^#]+(?:#[^#]+)?$" + } + ] + }, + { + "type": "object", + "properties": { + "providerID": { + "type": "string", + "allOf": [ + { + "pattern": "^[^/#]+$" + } + ] + }, + "model": { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + }, + "variant": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "providerID", + "model" + ], + "additionalProperties": false + } + ] + }, + "Config.Provider.Request": { + "type": "object", + "properties": { + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Agent": { + "type": "object", + "properties": { + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ModelSelection" + }, + { + "type": "null" + } + ] + }, + "request": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Provider.Request" + }, + { + "type": "null" + } + ] + }, + "system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + { + "type": "null" + } + ] + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^#[0-9a-fA-F]{6}$" + } + ] + }, + { + "type": "null" + } + ] + }, + "steps": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "permissions": { + "anyOf": [ + { + "$ref": "#/components/schemas/Permission.Ruleset" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Watcher": { + "type": "object", + "properties": { + "ignore": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Formatter.Entry": { + "type": "object", + "properties": { + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "command": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "environment": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "extensions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.LSP.Server": { + "type": "object", + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "extensions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "env": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "initialization": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "command" + ], + "additionalProperties": false + }, + "Config.Media.Image": { + "type": "object", + "properties": { + "auto_resize": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "max_width": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "max_height": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "max_base64_bytes": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Media": { + "type": "object", + "properties": { + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Media.Image" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.ToolOutput": { + "type": "object", + "properties": { + "max_lines": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "max_bytes": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.MCP": { + "type": "object", + "properties": { + "timeout": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.TimeoutConfig" + }, + { + "type": "null" + } + ] + }, + "servers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.LocalConfig" + }, + { + "$ref": "#/components/schemas/Mcp.RemoteConfig" + } + ] + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Compaction.Keep": { + "type": "object", + "properties": { + "tokens": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Compaction": { + "type": "object", + "properties": { + "auto": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "keep": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Compaction.Keep" + }, + { + "type": "null" + } + ] + }, + "buffer": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Command": { + "type": "object", + "properties": { + "template": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ModelSelection" + }, + { + "type": "null" + } + ] + }, + "subtask": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "template" + ], + "additionalProperties": false + }, + "Config.Reference.Git": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "repository" + ], + "additionalProperties": false + }, + "Config.Reference.Local": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + "ConfigWebSearch.Info": { + "type": "object", + "properties": { + "provider": { + "type": "string" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + "Config.Plugin.Entry": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "options": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "package" + ], + "additionalProperties": false + }, + "Config.Warming": { + "type": "object", + "properties": { + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Prompt sent for keep-alive requests" + }, + "interval": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Idle time between keep-alive requests (default: \"4 minutes\")" + }, + "duration": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Time after the last active request to keep a session warm (default: \"30 minutes\")" + } + }, + "additionalProperties": false + }, + "Config.Model.Cost.Cache": { + "type": "object", + "properties": { + "read": { + "anyOf": [ + { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + { + "type": "null" + } + ] + }, + "write": { + "anyOf": [ + { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Model.Cost": { + "type": "object", + "properties": { + "tier": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "context" + ] + }, + "size": { + "type": "integer" + } + }, + "required": [ + "type", + "size" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "input": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "output": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "cache": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Model.Cost.Cache" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "input", + "output" + ], + "additionalProperties": false + }, + "Config.Model.Limit": { + "type": "object", + "properties": { + "context": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "input": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "output": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Model": { + "type": "object", + "properties": { + "modelID": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "family": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "compatibility": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Compatibility" + }, + { + "type": "null" + } + ] + }, + "package": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "settings": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "capabilities": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Capabilities" + }, + { + "type": "null" + } + ] + }, + "variants": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + { + "type": "null" + } + ] + }, + "cost": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Model.Cost" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Config.Model.Cost" + } + } + ] + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "limit": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Model.Limit" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Provider": { + "type": "object", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "env": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "package": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "settings": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "models": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Model" + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ConfigExperimental.Info": { + "type": "object", + "properties": { + "subagent_depth": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum subagent nesting depth. Defaults to 1." + }, + "policies": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "provider.use" + ] + }, + "resource": { + "type": "string" + }, + "effect": { + "type": "string", + "enum": [ + "allow", + "deny" + ] + } + }, + "required": [ + "action", + "resource", + "effect" + ], + "additionalProperties": false + } + }, + { + "type": "null" + } + ], + "description": "Ordered policies controlling access to configured resources" + } + }, + "additionalProperties": false + }, + "Config.Info": { + "type": "object", + "properties": { + "$schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "JSON schema reference for configuration validation" + }, + "shell": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Default shell to use for terminal and shell tool execution" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ModelSelection" + }, + { + "type": "null" + } + ], + "description": "Default model to use when no session or agent model is selected" + }, + "default_agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Default primary agent to use when no session agent is selected" + }, + "autoupdate": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": [ + "notify" + ] + } + ] + }, + { + "type": "null" + } + ], + "description": "Automatically update or notify when a new version is available" + }, + "share": { + "anyOf": [ + { + "type": "string", + "enum": [ + "manual", + "auto", + "disabled" + ] + }, + { + "type": "null" + } + ], + "description": "Control whether sessions may be shared manually, automatically, or not at all" + }, + "enterprise": { + "anyOf": [ + { + "type": "object", + "properties": { + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ], + "description": "Enterprise sharing service configuration" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Username displayed in conversations and used for telemetry identity" + }, + "permissions": { + "anyOf": [ + { + "$ref": "#/components/schemas/Permission.Ruleset" + }, + { + "type": "null" + } + ], + "description": "Ordered tool permission rules applied to agent tool use" + }, + "agents": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Agent" + } + }, + { + "type": "null" + } + ], + "description": "Named built-in agent overrides and custom agent definitions" + }, + "snapshots": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Enable snapshots used for undo and revert behavior" + }, + "watcher": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Watcher" + }, + { + "type": "null" + } + ], + "description": "Filesystem watcher configuration" + }, + "formatter": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Formatter.Entry" + } + } + ] + }, + { + "type": "null" + } + ], + "description": "Enable built-in formatters or configure formatter overrides" + }, + "lsp": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "disabled" + ], + "additionalProperties": false + }, + { + "$ref": "#/components/schemas/Config.LSP.Server" + } + ] + } + } + ] + }, + { + "type": "null" + } + ], + "description": "Enable built-in language servers or configure server overrides" + }, + "media": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Media" + }, + { + "type": "null" + } + ], + "description": "Media processing configuration" + }, + "tool_output": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ToolOutput" + }, + { + "type": "null" + } + ], + "description": "Tool output truncation thresholds" + }, + "mcp": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.MCP" + }, + { + "type": "null" + } + ], + "description": "MCP server configuration" + }, + "compaction": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Compaction" + }, + { + "type": "null" + } + ], + "description": "Conversation compaction behavior" + }, + "skills": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Additional paths or URLs to discover skills from" + }, + "commands": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Command" + } + }, + { + "type": "null" + } + ], + "description": "Named slash command definitions" + }, + "instructions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Additional paths or URLs supplying ambient instructions" + }, + "references": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/Config.Reference.Git" + }, + { + "$ref": "#/components/schemas/Config.Reference.Local" + } + ] + } + }, + { + "type": "null" + } + ], + "description": "Named local directories or Git repositories available as external context" + }, + "websearch": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConfigWebSearch.Info" + }, + { + "type": "null" + } + ], + "description": "Web search provider selection" + }, + "plugins": { + "anyOf": [ + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/Config.Plugin.Entry" + } + ] + } + }, + { + "type": "null" + } + ], + "description": "Ordered plugin enablement directives and external package declarations" + }, + "warming": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/Config.Warming" + } + ] + }, + { + "type": "null" + } + ], + "description": "Keep recently active sessions warm with transient model requests (default: false)" + }, + "providers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Provider" + } + }, + { + "type": "null" + } + ] + }, + "experimental": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConfigExperimental.Info" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Document": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "document" + ] + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Config.Info" + } + }, + "required": [ + "type", + "info" + ], + "additionalProperties": false + }, + "Config.Directory": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "directory" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.File": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.AgentsDirectory": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agents" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.ClaudeDirectory": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "claude" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.Entry": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Document" + }, + { + "$ref": "#/components/schemas/Config.Directory" + }, + { + "$ref": "#/components/schemas/Config.File" + }, + { + "$ref": "#/components/schemas/Config.AgentsDirectory" + }, + { + "$ref": "#/components/schemas/Config.ClaudeDirectory" + } + ] } }, "securitySchemes": {} @@ -26571,6 +28363,10 @@ { "name": "websearch", "description": "Location-scoped web search routes." + }, + { + "name": "config", + "description": "Location-scoped configuration routes." } ] } diff --git a/packages/www/public/openapi.json b/packages/www/public/openapi.json index b33dfeb13be..51b542a6ff0 100644 --- a/packages/www/public/openapi.json +++ b/packages/www/public/openapi.json @@ -1025,14 +1025,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -1540,14 +1533,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -2912,14 +2898,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -3009,14 +2988,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -3586,14 +3558,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -3820,14 +3785,11 @@ "application/json": { "schema": { "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, { "$ref": "#/components/schemas/MessageNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" } ] } @@ -3956,14 +3918,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundError" - }, - { - "$ref": "#/components/schemas/SessionNotFoundError" - } - ] + "$ref": "#/components/schemas/SessionNotFoundError" } } } @@ -11771,39 +11726,101 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "required", - "running", - "completed" - ] - }, - "completed": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "required", + "completed" + ] } - ] + }, + "required": [ + "status" + ], + "additionalProperties": false }, - "total": { - "type": "integer", - "allOf": [ - { - "minimum": 0 + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running" + ] + }, + "progress": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "numerator": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "denominator": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "label" + ], + "additionalProperties": false } - ] + }, + "required": [ + "status", + "progress" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "error" + ] + }, + "error": { + "type": "string" + } + }, + "required": [ + "status", + "error" + ], + "additionalProperties": false } - }, - "required": [ - "status", - "completed", - "total" - ], - "additionalProperties": false + ] } } } @@ -11831,60 +11848,6 @@ }, "description": "Return the progress of the V1 to V2 session history migration.", "summary": "Get V1 migration status" - }, - "post": { - "tags": [ - "migration" - ], - "operationId": "v2.experimental.migration.v1.run", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "completed" - ] - } - }, - "required": [ - "status" - ], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestError" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - } - }, - "description": "Run or resume the V1 to V2 session history migration and wait for completion.", - "summary": "Run V1 migration" } }, "/api/websearch/provider": { @@ -12134,6 +12097,94 @@ "required": true } } + }, + "/api/config": { + "get": { + "tags": [ + "config" + ], + "operationId": "v2.config.get", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Config.Entry" + } + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Return configuration documents and discovery sources for the requested location, from lowest to highest priority.", + "summary": "Get configuration" + } } }, "components": { @@ -16014,6 +16065,9 @@ } ] } + }, + "text": { + "type": "string" } }, "required": [ @@ -26457,6 +26511,1744 @@ "results" ], "additionalProperties": false + }, + "Config.ModelSelection": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[^/#]+\\/[^#]+(?:#[^#]+)?$" + } + ] + }, + { + "type": "object", + "properties": { + "providerID": { + "type": "string", + "allOf": [ + { + "pattern": "^[^/#]+$" + } + ] + }, + "model": { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + }, + "variant": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^[^#]+$" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "providerID", + "model" + ], + "additionalProperties": false + } + ] + }, + "Config.Provider.Request": { + "type": "object", + "properties": { + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Agent": { + "type": "object", + "properties": { + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ModelSelection" + }, + { + "type": "null" + } + ] + }, + "request": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Provider.Request" + }, + { + "type": "null" + } + ] + }, + "system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + { + "type": "null" + } + ] + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string", + "allOf": [ + { + "pattern": "^#[0-9a-fA-F]{6}$" + } + ] + }, + { + "type": "null" + } + ] + }, + "steps": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "permissions": { + "anyOf": [ + { + "$ref": "#/components/schemas/Permission.Ruleset" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Watcher": { + "type": "object", + "properties": { + "ignore": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Formatter.Entry": { + "type": "object", + "properties": { + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "command": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "environment": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "extensions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.LSP.Server": { + "type": "object", + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "extensions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "env": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "initialization": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "command" + ], + "additionalProperties": false + }, + "Config.Media.Image": { + "type": "object", + "properties": { + "auto_resize": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "max_width": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "max_height": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "max_base64_bytes": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Media": { + "type": "object", + "properties": { + "image": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Media.Image" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.ToolOutput": { + "type": "object", + "properties": { + "max_lines": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + }, + "max_bytes": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "exclusiveMinimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.MCP": { + "type": "object", + "properties": { + "timeout": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.TimeoutConfig" + }, + { + "type": "null" + } + ] + }, + "servers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/components/schemas/Mcp.LocalConfig" + }, + { + "$ref": "#/components/schemas/Mcp.RemoteConfig" + } + ] + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Compaction.Keep": { + "type": "object", + "properties": { + "tokens": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Compaction": { + "type": "object", + "properties": { + "auto": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "keep": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Compaction.Keep" + }, + { + "type": "null" + } + ] + }, + "buffer": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Command": { + "type": "object", + "properties": { + "template": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ModelSelection" + }, + { + "type": "null" + } + ] + }, + "subtask": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "template" + ], + "additionalProperties": false + }, + "Config.Reference.Git": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "repository" + ], + "additionalProperties": false + }, + "Config.Reference.Local": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + "ConfigWebSearch.Info": { + "type": "object", + "properties": { + "provider": { + "type": "string" + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + "Config.Plugin.Entry": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "options": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "package" + ], + "additionalProperties": false + }, + "Config.Warming": { + "type": "object", + "properties": { + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Prompt sent for keep-alive requests" + }, + "interval": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Idle time between keep-alive requests (default: \"4 minutes\")" + }, + "duration": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Time after the last active request to keep a session warm (default: \"30 minutes\")" + } + }, + "additionalProperties": false + }, + "Config.Model.Cost.Cache": { + "type": "object", + "properties": { + "read": { + "anyOf": [ + { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + { + "type": "null" + } + ] + }, + "write": { + "anyOf": [ + { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Model.Cost": { + "type": "object", + "properties": { + "tier": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "context" + ] + }, + "size": { + "type": "integer" + } + }, + "required": [ + "type", + "size" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "input": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "output": { + "$ref": "#/components/schemas/Money.USDPerMillionTokens" + }, + "cache": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Model.Cost.Cache" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "input", + "output" + ], + "additionalProperties": false + }, + "Config.Model.Limit": { + "type": "object", + "properties": { + "context": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "input": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "output": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Model": { + "type": "object", + "properties": { + "modelID": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "family": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "compatibility": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Compatibility" + }, + { + "type": "null" + } + ] + }, + "package": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "settings": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "capabilities": { + "anyOf": [ + { + "$ref": "#/components/schemas/Model.Capabilities" + }, + { + "type": "null" + } + ] + }, + "variants": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "settings": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id" + ], + "additionalProperties": false + } + }, + { + "type": "null" + } + ] + }, + "cost": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Model.Cost" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Config.Model.Cost" + } + } + ] + }, + { + "type": "null" + } + ] + }, + "disabled": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "limit": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Model.Limit" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Provider": { + "type": "object", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "env": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "package": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "settings": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "headers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "body": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "models": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Model" + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ConfigExperimental.Info": { + "type": "object", + "properties": { + "subagent_depth": { + "anyOf": [ + { + "type": "integer", + "allOf": [ + { + "minimum": 0 + } + ] + }, + { + "type": "null" + } + ], + "description": "Maximum subagent nesting depth. Defaults to 1." + }, + "policies": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "provider.use" + ] + }, + "resource": { + "type": "string" + }, + "effect": { + "type": "string", + "enum": [ + "allow", + "deny" + ] + } + }, + "required": [ + "action", + "resource", + "effect" + ], + "additionalProperties": false + } + }, + { + "type": "null" + } + ], + "description": "Ordered policies controlling access to configured resources" + } + }, + "additionalProperties": false + }, + "Config.Info": { + "type": "object", + "properties": { + "$schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "JSON schema reference for configuration validation" + }, + "shell": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Default shell to use for terminal and shell tool execution" + }, + "model": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ModelSelection" + }, + { + "type": "null" + } + ], + "description": "Default model to use when no session or agent model is selected" + }, + "default_agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Default primary agent to use when no session agent is selected" + }, + "autoupdate": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": [ + "notify" + ] + } + ] + }, + { + "type": "null" + } + ], + "description": "Automatically update or notify when a new version is available" + }, + "share": { + "anyOf": [ + { + "type": "string", + "enum": [ + "manual", + "auto", + "disabled" + ] + }, + { + "type": "null" + } + ], + "description": "Control whether sessions may be shared manually, automatically, or not at all" + }, + "enterprise": { + "anyOf": [ + { + "type": "object", + "properties": { + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ], + "description": "Enterprise sharing service configuration" + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Username displayed in conversations and used for telemetry identity" + }, + "permissions": { + "anyOf": [ + { + "$ref": "#/components/schemas/Permission.Ruleset" + }, + { + "type": "null" + } + ], + "description": "Ordered tool permission rules applied to agent tool use" + }, + "agents": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Agent" + } + }, + { + "type": "null" + } + ], + "description": "Named built-in agent overrides and custom agent definitions" + }, + "snapshots": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Enable snapshots used for undo and revert behavior" + }, + "watcher": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Watcher" + }, + { + "type": "null" + } + ], + "description": "Filesystem watcher configuration" + }, + "formatter": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Formatter.Entry" + } + } + ] + }, + { + "type": "null" + } + ], + "description": "Enable built-in formatters or configure formatter overrides" + }, + "lsp": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "disabled" + ], + "additionalProperties": false + }, + { + "$ref": "#/components/schemas/Config.LSP.Server" + } + ] + } + } + ] + }, + { + "type": "null" + } + ], + "description": "Enable built-in language servers or configure server overrides" + }, + "media": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Media" + }, + { + "type": "null" + } + ], + "description": "Media processing configuration" + }, + "tool_output": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.ToolOutput" + }, + { + "type": "null" + } + ], + "description": "Tool output truncation thresholds" + }, + "mcp": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.MCP" + }, + { + "type": "null" + } + ], + "description": "MCP server configuration" + }, + "compaction": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Compaction" + }, + { + "type": "null" + } + ], + "description": "Conversation compaction behavior" + }, + "skills": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Additional paths or URLs to discover skills from" + }, + "commands": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Command" + } + }, + { + "type": "null" + } + ], + "description": "Named slash command definitions" + }, + "instructions": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Additional paths or URLs supplying ambient instructions" + }, + "references": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/Config.Reference.Git" + }, + { + "$ref": "#/components/schemas/Config.Reference.Local" + } + ] + } + }, + { + "type": "null" + } + ], + "description": "Named local directories or Git repositories available as external context" + }, + "websearch": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConfigWebSearch.Info" + }, + { + "type": "null" + } + ], + "description": "Web search provider selection" + }, + "plugins": { + "anyOf": [ + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/Config.Plugin.Entry" + } + ] + } + }, + { + "type": "null" + } + ], + "description": "Ordered plugin enablement directives and external package declarations" + }, + "warming": { + "anyOf": [ + { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/Config.Warming" + } + ] + }, + { + "type": "null" + } + ], + "description": "Keep recently active sessions warm with transient model requests (default: false)" + }, + "providers": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Config.Provider" + } + }, + { + "type": "null" + } + ] + }, + "experimental": { + "anyOf": [ + { + "$ref": "#/components/schemas/ConfigExperimental.Info" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Config.Document": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "document" + ] + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "info": { + "$ref": "#/components/schemas/Config.Info" + } + }, + "required": [ + "type", + "info" + ], + "additionalProperties": false + }, + "Config.Directory": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "directory" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.File": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.AgentsDirectory": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "agents" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.ClaudeDirectory": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "claude" + ] + }, + "path": { + "type": "string" + } + }, + "required": [ + "type", + "path" + ], + "additionalProperties": false + }, + "Config.Entry": { + "anyOf": [ + { + "$ref": "#/components/schemas/Config.Document" + }, + { + "$ref": "#/components/schemas/Config.Directory" + }, + { + "$ref": "#/components/schemas/Config.File" + }, + { + "$ref": "#/components/schemas/Config.AgentsDirectory" + }, + { + "$ref": "#/components/schemas/Config.ClaudeDirectory" + } + ] } }, "securitySchemes": {} @@ -26571,6 +28363,10 @@ { "name": "websearch", "description": "Location-scoped web search routes." + }, + { + "name": "config", + "description": "Location-scoped configuration routes." } ] }